Compare commits
182 Commits
v2.10.0-RC
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4aff418ed | ||
|
|
dd5c894b03 | ||
|
|
c3256b93c7 | ||
|
|
932f1f4a12 | ||
|
|
db3cbb0b35 | ||
|
|
c895274ebf | ||
|
|
a3647bca08 | ||
|
|
5fbf421d77 | ||
|
|
e09b3ee5f4 | ||
|
|
855960559d | ||
|
|
4cecfc6fc4 | ||
|
|
e62d1e4a98 | ||
|
|
6dddba0848 | ||
|
|
30fffdfe52 | ||
|
|
c8d6540ca6 | ||
|
|
2b2f08c231 | ||
|
|
0cfed90a7b | ||
|
|
01008dc1aa | ||
|
|
26b90cfdba | ||
|
|
b218051104 | ||
|
|
a2fa93938f | ||
|
|
c220d11dd0 | ||
|
|
40d533ecc5 | ||
|
|
d1a1c96e74 | ||
|
|
1a18985c0d | ||
|
|
8eede7b559 | ||
|
|
caab40b08e | ||
|
|
18fa320b19 | ||
|
|
6f9f6638d6 | ||
|
|
5688e2f7bc | ||
|
|
4738097178 | ||
|
|
591ecc3664 | ||
|
|
b9089a39ac | ||
|
|
b7bdf89d37 | ||
|
|
849ea9f7b2 | ||
|
|
bd737ef563 | ||
|
|
e250bd38f8 | ||
|
|
7083da702a | ||
|
|
3d01c20339 | ||
|
|
3b74649206 | ||
|
|
a10f0c992e | ||
|
|
32974620c4 | ||
|
|
0781e3ad8e | ||
|
|
0ce107eea0 | ||
|
|
73e3efd92f | ||
|
|
69bff8d8fe | ||
|
|
22b223f82f | ||
|
|
7b0d43c8b8 | ||
|
|
7c64fb7a11 | ||
|
|
61f8b69f02 | ||
|
|
c32fedeb4c | ||
|
|
4ba3bed742 | ||
|
|
810318b584 | ||
|
|
863fda3ef3 | ||
|
|
deb7715ea1 | ||
|
|
adaebb17e7 | ||
|
|
e3f6ab43ff | ||
|
|
cf977b7612 | ||
|
|
fedb60fc8f | ||
|
|
f8ddf88b8c | ||
|
|
301581da63 | ||
|
|
88cb945760 | ||
|
|
c61d0052a9 | ||
|
|
147c3a1153 | ||
|
|
e9f1e4d8da | ||
|
|
f3a995f68f | ||
|
|
a967163d28 | ||
|
|
d3514213ca | ||
|
|
a55341f406 | ||
|
|
1c9a5bfa18 | ||
|
|
179900fab0 | ||
|
|
9011e30521 | ||
|
|
418f4ce576 | ||
|
|
9eb65237f9 | ||
|
|
89a667569b | ||
|
|
b87091a33a | ||
|
|
d9a659acbc | ||
|
|
ec8347fd7d | ||
|
|
b161729c46 | ||
|
|
2194a7fc23 | ||
|
|
03c6471e97 | ||
|
|
be52ed7d44 | ||
|
|
3afc9db5d3 | ||
|
|
30d882e1c0 | ||
|
|
4836279b77 | ||
|
|
653cd758f4 | ||
|
|
3f9478de30 | ||
|
|
244692e766 | ||
|
|
ae0c529fca | ||
|
|
356188a350 | ||
|
|
549627a355 | ||
|
|
fab4467bfd | ||
|
|
871efc88b5 | ||
|
|
caa1fd4d9b | ||
|
|
a33740c372 | ||
|
|
bcd9c692e6 | ||
|
|
4bbfbb82bc | ||
|
|
d4aff8067e | ||
|
|
a1889c88d3 | ||
|
|
745e76db79 | ||
|
|
1c05d8df28 | ||
|
|
8a92912035 | ||
|
|
4cb4fe53a5 | ||
|
|
eb48d77636 | ||
|
|
8781c78c15 | ||
|
|
039e641046 | ||
|
|
b3053f32b2 | ||
|
|
98562fd967 | ||
|
|
573ecbd58d | ||
|
|
e9d16ee172 | ||
|
|
cf17f03ae0 | ||
|
|
e287705e88 | ||
|
|
2698993a6d | ||
|
|
d7957b85ae | ||
|
|
ea0e7340cc | ||
|
|
566dbbb157 | ||
|
|
908faa491b | ||
|
|
016ac336b9 | ||
|
|
1838029d22 | ||
|
|
b1cfa6db19 | ||
|
|
8c676eb965 | ||
|
|
1808206e74 | ||
|
|
42d274e988 | ||
|
|
d006b89efd | ||
|
|
8d23168a87 | ||
|
|
31a37ae506 | ||
|
|
b829003711 | ||
|
|
0e8df4441d | ||
|
|
14c88280f5 | ||
|
|
02095595c6 | ||
|
|
b69e7e4ddf | ||
|
|
33dedbbfa3 | ||
|
|
3b7dad989f | ||
|
|
538c6b5233 | ||
|
|
5c7f683d16 | ||
|
|
628e4fa873 | ||
|
|
89ee99f98d | ||
|
|
d21f55b476 | ||
|
|
09e3327e94 | ||
|
|
0aed1ba04f | ||
|
|
b52308df80 | ||
|
|
6273020127 | ||
|
|
95963eb0be | ||
|
|
7ac82d4ad9 | ||
|
|
c23cce672c | ||
|
|
d39b4b3624 | ||
|
|
86f2ffa61f | ||
|
|
7d72ad0f37 | ||
|
|
c21bf5b87d | ||
|
|
d80d5557c8 | ||
|
|
d72867da35 | ||
|
|
11e2bf4cbb | ||
|
|
58f8a02557 | ||
|
|
f9e08d53af | ||
|
|
8132012534 | ||
|
|
7312980453 | ||
|
|
8c20d41d89 | ||
|
|
d50d53ac1f | ||
|
|
b0d7ab2e81 | ||
|
|
91223b7518 | ||
|
|
5170ea81e7 | ||
|
|
2229e8cb7d | ||
|
|
187065098b | ||
|
|
2fc93fa819 | ||
|
|
0f40c4cd49 | ||
|
|
7ad184cb48 | ||
|
|
41a7d5d0a8 | ||
|
|
7106d61881 | ||
|
|
52ca7d3503 | ||
|
|
7d504da0a8 | ||
|
|
680c4a9ec4 | ||
|
|
3887d57fa4 | ||
|
|
de8651ab52 | ||
|
|
316224b424 | ||
|
|
6eae6c046e | ||
|
|
71431f8fb5 | ||
|
|
674682e919 | ||
|
|
a5ca8115af | ||
|
|
d4338fce42 | ||
|
|
e925f07505 | ||
|
|
fb64813b2a | ||
|
|
6c437459ca |
6
.github/workflows/ci.yml
vendored
@@ -20,13 +20,13 @@ defaults:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_REGISTRY_URL: tip-tip-wlan-cloud-ucentral.jfrog.io
|
||||
DOCKER_REGISTRY_USERNAME: ucentral
|
||||
steps:
|
||||
- name: Checkout actions repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: Telecominfraproject/.github
|
||||
path: github
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- docker
|
||||
steps:
|
||||
- name: Checkout actions repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: Telecominfraproject/.github
|
||||
path: github
|
||||
|
||||
2
.github/workflows/enforce-jira-issue-key.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout actions repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: Telecominfraproject/.github
|
||||
path: github
|
||||
|
||||
4
.github/workflows/release.yml
vendored
@@ -11,13 +11,13 @@ defaults:
|
||||
|
||||
jobs:
|
||||
helm-package:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HELM_REPO_URL: https://tip.jfrog.io/artifactory/tip-wlan-cloud-ucentral-helm/
|
||||
HELM_REPO_USERNAME: ucentral
|
||||
steps:
|
||||
- name: Checkout uCentral assembly chart repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: wlan-cloud-ucentralgw-ui
|
||||
|
||||
|
||||
1
.gitignore
vendored
@@ -18,3 +18,4 @@
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
.vscode/settings.json
|
||||
|
||||
12
Dockerfile
@@ -1,6 +1,8 @@
|
||||
FROM node:14-alpine3.11 AS build
|
||||
FROM node:18.7.0-alpine3.15 AS build
|
||||
|
||||
COPY package.json package-lock.json /
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json /app/
|
||||
|
||||
RUN npm install
|
||||
|
||||
@@ -8,8 +10,8 @@ COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.20.1-alpine AS runtime
|
||||
FROM nginx:1.22.0-alpine AS runtime
|
||||
|
||||
COPY --from=build /build/ /usr/share/nginx/html/
|
||||
COPY --from=build /app/build/ /usr/share/nginx/html/
|
||||
|
||||
COPY --from=build docker-entrypoint.d/40-generate-config.sh /docker-entrypoint.d/40-generate-config.sh
|
||||
COPY --from=build /app/docker-entrypoint.d/40-generate-config.sh /docker-entrypoint.d/40-generate-config.sh
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
#!/bin/ash
|
||||
# Check if variables are set
|
||||
export DEFAULT_OWSEC_URL="${DEFAULT_OWSEC_URL:-https://ucentral.dpaas.arilia.com:16001}"
|
||||
export ALLOW_OWSEC_CHANGE="${ALLOW_OWSEC_CHANGE:-false}"
|
||||
|
||||
echo '{"DEFAULT_UCENTRALSEC_URL": "'$DEFAULT_UCENTRALSEC_URL'","ALLOW_UCENTRALSEC_CHANGE": '$ALLOW_UCENTRALSEC_CHANGE'}' > /usr/share/nginx/html/config.json
|
||||
ENV_CONFIG_PATH=/usr/share/nginx/html/env-config.js
|
||||
|
||||
# Recreate config file
|
||||
rm -rf $ENV_CONFIG_PATH
|
||||
touch $ENV_CONFIG_PATH
|
||||
|
||||
# Add assignment
|
||||
echo "window._env_ = {" >> $ENV_CONFIG_PATH
|
||||
|
||||
# Read each line in .env file
|
||||
# Each line represents key=value pairs
|
||||
env | grep REACT_ | while read -r line || [[ -n "$line" ]];
|
||||
do
|
||||
echo $line
|
||||
# Split env variables by character `=`
|
||||
if printf '%s\n' "$line" | grep -q -e '='; then
|
||||
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
|
||||
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
|
||||
fi
|
||||
|
||||
# Read value of current variable if exists as Environment variable
|
||||
value=$(printf '%s\n' "${!varname}")
|
||||
# Otherwise use value from .env file
|
||||
[[ -z $value ]] && value=${varvalue}
|
||||
|
||||
# Append configuration property to JS file
|
||||
echo " $varname: \"$value\"," >> $ENV_CONFIG_PATH
|
||||
done
|
||||
|
||||
echo "}" >> $ENV_CONFIG_PATH
|
||||
|
||||
@@ -17,7 +17,9 @@ metadata:
|
||||
{{- end }}
|
||||
|
||||
spec:
|
||||
|
||||
{{- if $ingressValue.className }}
|
||||
ingressClassName: {{ $ingressValue.className }}
|
||||
{{- end }}
|
||||
{{- if $ingressValue.tls }}
|
||||
tls:
|
||||
{{- range $ingressValue.tls }}
|
||||
|
||||
@@ -75,5 +75,4 @@ podAnnotations: {}
|
||||
|
||||
# Application
|
||||
public_env_variables:
|
||||
DEFAULT_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001
|
||||
ALLOW_UCENTRALSEC_CHANGE: false
|
||||
REACT_APP_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001
|
||||
|
||||
990
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.10.0(49)",
|
||||
"version": "4.1.0",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
@@ -15,8 +15,10 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/anatomy": "^2.1.1",
|
||||
"@chakra-ui/icons": "^2.0.18",
|
||||
"@chakra-ui/react": "^2.3.6",
|
||||
"@chakra-ui/styled-system": "^2.9.0",
|
||||
"@chakra-ui/theme-tools": "^2.0.12",
|
||||
"@chakra-ui/utils": "^2.0.14",
|
||||
"@emotion/react": "^11.10.6",
|
||||
@@ -58,7 +60,7 @@
|
||||
"react-virtualized-auto-sizer": "^1.0.15",
|
||||
"react-window": "^1.8.9",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "^5.0.4",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^4.2.1",
|
||||
"yup": "^0.32.11",
|
||||
@@ -92,6 +94,7 @@
|
||||
"lint-staged": "^13.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
BIN
public/devices/asterfusion_CX204Y-24GT-M-SWP4.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
public/devices/asterfusion_CX204Y-48GT-M-SWP4.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
public/devices/cig_wf186h.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
public/devices/cig_wf186w.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
public/devices/cig_wf188n-ca-ath12.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/devices/cig_wf188n-ca.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/devices/cig_wf188n-us.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
public/devices/cig_wf196-ca-ath12.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
public/devices/cig_wf196-ca.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
public/devices/cig_wf196-us.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
public/devices/cig_wf610d.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/devices/cig_wf660a.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
public/devices/cybertan_eww631-a1.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
public/devices/cybertan_eww631-b1.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
public/devices/cybertan_skf224-c1.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/devices/cybertan_skf424-c1.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
public/devices/edgecore_eap101-ath12.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
public/devices/edgecore_eap102-ath12.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
public/devices/edgecore_eap104.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
public/devices/edgecore_eap104_ath12.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
public/devices/edgecore_eap111.png
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
public/devices/edgecore_ecs2100-10p.png
Normal file
|
After Width: | Height: | Size: 502 KiB |
BIN
public/devices/edgecore_ecs2100-10t.png
Normal file
|
After Width: | Height: | Size: 637 KiB |
BIN
public/devices/edgecore_ecs2100-28p.png
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
public/devices/edgecore_ecs2100-28pp.png
Normal file
|
After Width: | Height: | Size: 313 KiB |
BIN
public/devices/edgecore_ecs2100-28t.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
public/devices/edgecore_ecs2100-52t.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
public/devices/edgecore_ecs4125-10p.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
public/devices/edgecore_ecs4125.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/devices/edgecore_ecs4150-28t.png
Normal file
|
After Width: | Height: | Size: 267 KiB |
BIN
public/devices/edgecore_ecs4150-58p.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
public/devices/edgecore_oap101-6e.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
public/devices/edgecore_oap101.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
public/devices/edgecore_oap101e-6e.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
public/devices/edgecore_oap101e.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
public/devices/edgecore_oap102.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
public/devices/hfcl_ion4x.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/devices/hfcl_ion4x_2.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/devices/hfcl_ion4x_w.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
public/devices/hfcl_ion4xe.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
public/devices/hfcl_ion4xi.png
Normal file
|
After Width: | Height: | Size: 817 KiB |
BIN
public/devices/hfcl_ion4xi_HMR.png
Normal file
|
After Width: | Height: | Size: 879 KiB |
BIN
public/devices/hfcl_ion4xi_w.png
Normal file
|
After Width: | Height: | Size: 664 KiB |
BIN
public/devices/hfcl_ion4xi_wp.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
@@ -223,6 +223,7 @@
|
||||
"day": "Tag",
|
||||
"days": "Tage",
|
||||
"default": "Standard",
|
||||
"defaults": "Standardeinstellungen",
|
||||
"description": "Beschreibung",
|
||||
"details": "Einzelheiten",
|
||||
"device_details": "Gerätedetails",
|
||||
@@ -268,6 +269,7 @@
|
||||
"map": "Karte",
|
||||
"max": "Max",
|
||||
"min": "MINDEST",
|
||||
"miscellaneous": "Verschiedenes",
|
||||
"mode": "Modus",
|
||||
"model": "Modell",
|
||||
"modified": "Geändert",
|
||||
@@ -702,11 +704,32 @@
|
||||
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
|
||||
},
|
||||
"firmware": {
|
||||
"confirm_default_data": "Bitte bestätigen Sie die untenstehenden Informationen und klicken Sie auf „Bestätigen“, sobald Sie bereit sind, den Vorgang zu starten",
|
||||
"create_success": "Neue Standard-Firmware-Einstellungen erstellt!",
|
||||
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
|
||||
"default_created_error_one": "{{count}} Fehler beim Versuch, eine neue Einstellung zu erstellen",
|
||||
"default_created_error_other": "{{count}} Fehler beim Versuch, eine neue Einstellung zu erstellen",
|
||||
"default_created_one": "{{count}} Standard-Firmware-Einstellung erstellt",
|
||||
"default_created_other": "{{count}} Standard-Firmware-Einstellungen erstellt",
|
||||
"default_found_one": "Für den Gerätetyp {{count}} wurde eine gültige Revision gefunden",
|
||||
"default_found_other": "Gültige Revisionen für {{count}} Gerätetypen gefunden",
|
||||
"default_mass_delete_success_one": " {{count}} Standard-Firmware-Einstellung gelöscht!",
|
||||
"default_mass_delete_success_other": " {{count}} Standard-Firmware-Einstellungen gelöscht!",
|
||||
"default_not_found_one": "Keine gültigen Firmware-Versionen für den Gerätetyp {{count}} ",
|
||||
"default_not_found_other": "Keine gültigen Firmware-Versionen für {{count}} Gerätetypen",
|
||||
"default_title": "",
|
||||
"default_update_success": "Standard-Firmware für {{deviceType}}aktualisiert!",
|
||||
"delete_success": "Standard-Firmware-Einstellung gelöscht!",
|
||||
"edit_default_title": "Dies ist die aktuelle Firmware, die als Mindestversion für neue APs vom Typ {{deviceType}}verwendet wird. Wenn ein neuer {{deviceType}} AP eine Verbindung zum Gateway herstellt, wird er automatisch auf diese Version aktualisiert.",
|
||||
"fetching_defaults": "Alle verfügbaren Firmware für ausgewählte Gerätetypen werden abgerufen...",
|
||||
"last_db_update_modal": "Firmware-Datenbank",
|
||||
"last_db_update_title": "Datenbank",
|
||||
"one": "Firmware",
|
||||
"select_default_device_types": "Bitte wählen Sie alle Gerätetypen aus, auf die Sie diese neue Standard-Firmware-Regel anwenden möchten. Wenn Sie den gewünschten Gerätetyp nicht finden können, bedeutet dies, dass bereits eine Regel angewendet wurde.",
|
||||
"select_default_revision": "Sie können jetzt die Mindestversion auswählen, auf die Ihre Gerätetypen abzielen sollen",
|
||||
"start_db_update": "Datenbankaktualisierung starten",
|
||||
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
|
||||
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern",
|
||||
"update_success": "Standard-Firmware-Informationen gespeichert!"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Unterstützt von",
|
||||
@@ -715,6 +738,7 @@
|
||||
"form": {
|
||||
"captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)",
|
||||
"certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.",
|
||||
"invalid_alphanumeric_with_dash": "Akzeptierte Zeichen. sind nur alphanumerisch (Buchstaben & Zahlen)",
|
||||
"invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12",
|
||||
"invalid_email": "Ungültige E-Mail",
|
||||
"invalid_file_content": "Ungültiger Dateiinhalt, bitte bestätigen Sie, dass es sich um ein gültiges Format handelt",
|
||||
@@ -741,7 +765,11 @@
|
||||
"invalid_static_ipv4_e": "Ungültige Adresse, dieser Bereich ist für Experimente reserviert (Klasse E). Das erste Oktett sollte 223 oder niedriger sein",
|
||||
"invalid_third_party": "Ungültige Drittanbieter-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert ein gültiges JSON ist",
|
||||
"key_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN PRIVATE KEY-----“ beginnt und mit „-----END PRIVATE KEY-----“ endet.",
|
||||
"max_length": "Maximale Länge von {{max}} Zeichen.",
|
||||
"max_value": "Maximalwert von {{max}}",
|
||||
"min_length": "Mindestlänge von {{min}} Zeichen.",
|
||||
"min_max_string": "Der Wert muss eine Länge zwischen {{min}} (einschließlich) und {{max}} (einschließlich) haben.",
|
||||
"min_value": "Mindestwert von {{min}}",
|
||||
"missing_interface_upstream": "Sie müssen mindestens eine Upstream-Schnittstelle haben. Im Moment sind alle Ihre Schnittstellen nachgelagert",
|
||||
"new_email_to_notify": "Neue E-Mail zur Benachrichtigung",
|
||||
"new_phone_to_notify": "Neues Telefon zu benachrichtigen",
|
||||
@@ -883,6 +911,11 @@
|
||||
"one": "Benachrichtigung",
|
||||
"other": "Benachrichtigungen"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Pool-Strategie",
|
||||
"radius_endpoint_one": "Radiusendpunkt",
|
||||
"radius_endpoint_other": "Radiusendpunkte"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
|
||||
"delete_operator": "Betreiber löschen",
|
||||
@@ -948,6 +981,27 @@
|
||||
"title": "Beschränkungen",
|
||||
"tty": "TTY-Zugriff"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "Neues Konto erstellt!",
|
||||
"account_deleted": "Konto gelöscht!",
|
||||
"account_one": "Konto",
|
||||
"account_other": "Konten",
|
||||
"certificate_deleted": "Zertifikat gelöscht!",
|
||||
"certificate_one": "Zertifikat",
|
||||
"certificate_other": "Zertifikate",
|
||||
"city": "Stadt",
|
||||
"common_name": "Gemeinsamen Namen",
|
||||
"country": "Land",
|
||||
"global_reach": "Globale Reichweite",
|
||||
"global_reach_account_id": "Konto-ID",
|
||||
"invalid_certificate": "Ungültiges Zertifikat",
|
||||
"invalid_key": "Ungültiger privater Schlüssel",
|
||||
"location_details_title": "Ort",
|
||||
"organization": "Organisation",
|
||||
"private_key": "Privat Schlüssel",
|
||||
"province": "Provinz",
|
||||
"state": "Zustand"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithmus",
|
||||
"algorithm_other": "Algorithmen",
|
||||
@@ -1008,6 +1062,9 @@
|
||||
"current_live_devices": "Aktuelle Live-Geräte",
|
||||
"currently_running_one": "Derzeit wird {{count}} Simulation ausgeführt",
|
||||
"currently_running_other": "Derzeit laufen {{count}} Simulationen",
|
||||
"delete_devices_confirm": "Sind Sie sicher, dass Sie alle Geräte und deren Statistiken vom Gateway entfernen möchten? Diese Aktion ist nicht rückgängig zu machen",
|
||||
"delete_devices_loading": "Dieser Vorgang kann bis zu 5 Minuten dauern",
|
||||
"delete_simulation_devices": "Geräte löschen",
|
||||
"delete_success": "Gelöschte Simulation!",
|
||||
"duration": "Dauer",
|
||||
"error_devices": "Fehler Geräte",
|
||||
@@ -1066,6 +1123,7 @@
|
||||
"title": "Abonnenten"
|
||||
},
|
||||
"system": {
|
||||
"advanced": "Erweitert",
|
||||
"backend_logs": "Back-End-Protokolle",
|
||||
"configuration": "Aufbau",
|
||||
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"day": "Día",
|
||||
"days": "días",
|
||||
"default": "Defecto",
|
||||
"defaults": "Valores predeterminados",
|
||||
"description": "Descripción",
|
||||
"details": "Detalles",
|
||||
"device_details": "Detalles del dispositivo",
|
||||
@@ -268,6 +269,7 @@
|
||||
"map": "Mapa",
|
||||
"max": "Max",
|
||||
"min": "Min",
|
||||
"miscellaneous": "Diverso",
|
||||
"mode": "Modo",
|
||||
"model": "Modelo",
|
||||
"modified": "Modificado",
|
||||
@@ -702,11 +704,32 @@
|
||||
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
|
||||
},
|
||||
"firmware": {
|
||||
"confirm_default_data": "Confirme la información a continuación y haga clic en 'Confirmar' una vez que esté listo para comenzar el proceso",
|
||||
"create_success": "¡Se crearon nuevas configuraciones de firmware predeterminadas!",
|
||||
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
|
||||
"default_created_error_one": "{{count}} error al intentar crear una nueva configuración",
|
||||
"default_created_error_other": "{{count}} errores al intentar crear una nueva configuración",
|
||||
"default_created_one": "{{count}} configuración de firmware predeterminada creada",
|
||||
"default_created_other": "{{count}} ajustes de firmware predeterminados creados",
|
||||
"default_found_one": "Se encontró una revisión válida para el tipo de dispositivo {{count}} ",
|
||||
"default_found_other": "Se encontraron revisiones válidas para {{count}} tipos de dispositivos",
|
||||
"default_mass_delete_success_one": "¡Se eliminó {{count}} configuración de firmware predeterminada!",
|
||||
"default_mass_delete_success_other": "¡Se eliminaron {{count}} configuraciones de firmware predeterminadas!",
|
||||
"default_not_found_one": "No hay versiones de firmware válidas para el tipo de dispositivo {{count}} ",
|
||||
"default_not_found_other": "No hay versiones de firmware válidas para {{count}} tipos de dispositivos",
|
||||
"default_title": "",
|
||||
"default_update_success": "¡Firmware predeterminado actualizado para {{deviceType}}!",
|
||||
"delete_success": "¡Configuración de firmware predeterminada eliminada!",
|
||||
"edit_default_title": "Este es el firmware actual que se utiliza como versión mínima para los nuevos AP de tipo {{deviceType}}. Si un nuevo AP {{deviceType}} se conecta a la puerta de enlace, se actualizará automáticamente a esta versión.",
|
||||
"fetching_defaults": "Obteniendo todo el firmware disponible para los tipos de dispositivos seleccionados...",
|
||||
"last_db_update_modal": "Base de datos de firmware",
|
||||
"last_db_update_title": "Base de datos",
|
||||
"one": "Firmware",
|
||||
"select_default_device_types": "Seleccione todos los tipos de dispositivos a los que desea apuntar con esta nueva regla de firmware predeterminada. Si no puede encontrar el tipo de dispositivo deseado, significa que ya tienen una regla aplicada.",
|
||||
"select_default_revision": "Ahora puede seleccionar la revisión mínima a la que desea que se dirijan sus tipos de dispositivos",
|
||||
"start_db_update": "Iniciar actualización de la base de datos",
|
||||
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
|
||||
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse",
|
||||
"update_success": "¡Información de firmware predeterminada guardada!"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "energizado por",
|
||||
@@ -715,6 +738,7 @@
|
||||
"form": {
|
||||
"captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)",
|
||||
"certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"",
|
||||
"invalid_alphanumeric_with_dash": "Caracteres aceptados. son solo alfanuméricos (letras y números)",
|
||||
"invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12",
|
||||
"invalid_email": "Email inválido",
|
||||
"invalid_file_content": "Contenido de archivo no válido, confirme que tiene un formato válido",
|
||||
@@ -741,7 +765,11 @@
|
||||
"invalid_static_ipv4_e": "Dirección no válida, este rango reservado para experimentos (clase E). El primer octeto debe ser 223 o inferior",
|
||||
"invalid_third_party": "Cadena JSON de terceros no válida. Confirme que su valor es un JSON válido",
|
||||
"key_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN PRIVATE KEY-----\" y termine con \"-----END PRIVATE KEY-----\"",
|
||||
"max_length": "Longitud máxima de {{max}} caracteres.",
|
||||
"max_value": "Valor máximo de {{max}}",
|
||||
"min_length": "Longitud mínima de {{min}} caracteres.",
|
||||
"min_max_string": "El valor debe tener una longitud entre {{min}} (inclusive) y {{max}} (inclusive)",
|
||||
"min_value": "Valor mínimo de {{min}}",
|
||||
"missing_interface_upstream": "Debe tener al menos una interfaz ascendente. Por el momento, todas sus interfaces están en sentido descendente",
|
||||
"new_email_to_notify": "Nuevo correo electrónico para notificar",
|
||||
"new_phone_to_notify": "Nuevo teléfono para avisar",
|
||||
@@ -883,6 +911,11 @@
|
||||
"one": "Notificación",
|
||||
"other": "Notificaciones"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Estrategia de piscina",
|
||||
"radius_endpoint_one": "Punto final del radio",
|
||||
"radius_endpoint_other": "Puntos finales de radio"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
|
||||
"delete_operator": "Eliminar operador",
|
||||
@@ -948,6 +981,27 @@
|
||||
"title": "Las restricciones",
|
||||
"tty": "Acceso TTY"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "¡Nueva cuenta creada!",
|
||||
"account_deleted": "¡Cuenta eliminada!",
|
||||
"account_one": "Cuenta",
|
||||
"account_other": "Cuentas",
|
||||
"certificate_deleted": "Certificado eliminado!",
|
||||
"certificate_one": "Certificado",
|
||||
"certificate_other": "Certificados",
|
||||
"city": "ciudad",
|
||||
"common_name": "Nombre común",
|
||||
"country": "País",
|
||||
"global_reach": "Alcance global",
|
||||
"global_reach_account_id": "ID de cuenta ",
|
||||
"invalid_certificate": "Certificado inválido",
|
||||
"invalid_key": "Clave privada no válida",
|
||||
"location_details_title": "Ubicación",
|
||||
"organization": "Organización",
|
||||
"private_key": "Llave privada",
|
||||
"province": "Provincia",
|
||||
"state": "Estado"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algoritmo",
|
||||
"algorithm_other": "Algoritmos",
|
||||
@@ -1008,6 +1062,9 @@
|
||||
"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_devices_confirm": "¿Está seguro de que desea eliminar todos los dispositivos y sus estadísticas de la puerta de enlace? Esta acción no es reversible",
|
||||
"delete_devices_loading": "Este proceso puede tardar hasta 5 minutos.",
|
||||
"delete_simulation_devices": "BORRAR DISPOSITIVOS",
|
||||
"delete_success": "¡Simulación eliminada!",
|
||||
"duration": "Duración",
|
||||
"error_devices": "Dispositivos de error",
|
||||
@@ -1066,6 +1123,7 @@
|
||||
"title": "Suscriptores"
|
||||
},
|
||||
"system": {
|
||||
"advanced": "Avanzado",
|
||||
"backend_logs": "Registros de back-end",
|
||||
"configuration": "Configuración",
|
||||
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"day": "journée",
|
||||
"days": "Journées",
|
||||
"default": "Défaut",
|
||||
"defaults": "Valeurs par défaut",
|
||||
"description": "La description",
|
||||
"details": "Détails",
|
||||
"device_details": "Détails de l'appareil",
|
||||
@@ -268,6 +269,7 @@
|
||||
"map": "Carte",
|
||||
"max": "Max",
|
||||
"min": "Min",
|
||||
"miscellaneous": "Divers",
|
||||
"mode": "Mode",
|
||||
"model": "Modèle",
|
||||
"modified": "Modifié",
|
||||
@@ -702,11 +704,32 @@
|
||||
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
|
||||
},
|
||||
"firmware": {
|
||||
"confirm_default_data": "Veuillez confirmer les informations ci-dessous et cliquez sur \"Confirmer\" une fois que vous êtes prêt à démarrer le processus",
|
||||
"create_success": "Création de nouveaux paramètres de firmware par défaut !",
|
||||
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
|
||||
"default_created_error_one": "{{count}} erreur lors de la tentative de création d'un nouveau paramètre",
|
||||
"default_created_error_other": "{{count}} erreurs lors de la tentative de création d'un nouveau paramètre",
|
||||
"default_created_one": "{{count}} paramètre de micrologiciel par défaut créé",
|
||||
"default_created_other": "{{count}} paramètres de micrologiciel par défaut créés",
|
||||
"default_found_one": "Révision valide trouvée pour le type d'appareil {{count}} ",
|
||||
"default_found_other": "Révisions valides trouvées pour {{count}} types d'appareils",
|
||||
"default_mass_delete_success_one": "Paramètre de micrologiciel par défaut {{count}} supprimé !",
|
||||
"default_mass_delete_success_other": " {{count}} paramètres de micrologiciel par défaut supprimés !",
|
||||
"default_not_found_one": "Aucune version de micrologiciel valide pour le type d'appareil {{count}} ",
|
||||
"default_not_found_other": "Aucune version de micrologiciel valide pour {{count}} types d'appareils",
|
||||
"default_title": "",
|
||||
"default_update_success": "Firmware par défaut mis à jour pour {{deviceType}} !",
|
||||
"delete_success": "Paramètre de micrologiciel par défaut supprimé !",
|
||||
"edit_default_title": "Il s'agit du micrologiciel actuel utilisé comme version minimale pour les nouveaux points d'accès de type {{deviceType}}. Si un nouveau point d'accès {{deviceType}} se connecte à la passerelle, il sera automatiquement mis à niveau vers cette version.",
|
||||
"fetching_defaults": "Récupération de tous les micrologiciels disponibles pour les types d'appareils sélectionnés...",
|
||||
"last_db_update_modal": "Base de données du micrologiciel",
|
||||
"last_db_update_title": "Base de données",
|
||||
"one": "Micrologiciel",
|
||||
"select_default_device_types": "Veuillez sélectionner tous les types d'appareils que vous souhaitez cibler avec cette nouvelle règle de micrologiciel par défaut. Si vous ne trouvez pas le type d'appareil souhaité, cela signifie qu'une règle est déjà appliquée.",
|
||||
"select_default_revision": "Vous pouvez maintenant sélectionner la révision minimale que vous souhaitez que vos types d'appareils ciblent",
|
||||
"start_db_update": "Démarrer la mise à jour de la base de données",
|
||||
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
|
||||
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes",
|
||||
"update_success": "Informations sur le micrologiciel par défaut enregistrées !"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Alimenté par",
|
||||
@@ -715,6 +738,7 @@
|
||||
"form": {
|
||||
"captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)",
|
||||
"certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"",
|
||||
"invalid_alphanumeric_with_dash": "Caractères acceptés. sont uniquement alphanumériques (lettres et chiffres)",
|
||||
"invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12",
|
||||
"invalid_email": "Email Invalide",
|
||||
"invalid_file_content": "Contenu de fichier non valide, veuillez confirmer qu'il est au format valide",
|
||||
@@ -741,7 +765,11 @@
|
||||
"invalid_static_ipv4_e": "Adresse invalide, cette plage est réservée aux expérimentations (classe E). Le premier octet doit être 223 ou moins",
|
||||
"invalid_third_party": "Chaîne JSON tierce non valide. Veuillez confirmer que votre valeur est un JSON valide",
|
||||
"key_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN PRIVATE KEY-----\" et se termine par \"-----END PRIVATE KEY-----\"",
|
||||
"max_length": "Longueur maximale de {{max}} caractères.",
|
||||
"max_value": "Valeur maximale de {{max}}",
|
||||
"min_length": "Longueur minimale de {{min}} caractères.",
|
||||
"min_max_string": "La valeur doit être d'une longueur comprise entre {{min}} (inclus) et {{max}} (inclus)",
|
||||
"min_value": "Valeur minimale de {{min}}",
|
||||
"missing_interface_upstream": "Vous devez avoir au moins une interface en amont. Pour le moment, toutes vos interfaces sont en aval",
|
||||
"new_email_to_notify": "Nouvel e-mail à notifier",
|
||||
"new_phone_to_notify": "Nouveau téléphone à notifier",
|
||||
@@ -883,6 +911,11 @@
|
||||
"one": "Notification",
|
||||
"other": "Les notifications"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Stratégie de pool",
|
||||
"radius_endpoint_one": "Point final de rayon",
|
||||
"radius_endpoint_other": "Points de terminaison du rayon"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
|
||||
"delete_operator": "Supprimer l'opérateur",
|
||||
@@ -948,6 +981,27 @@
|
||||
"title": "Restrictions",
|
||||
"tty": "Accès ATS"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "Nouveau compte créé !",
|
||||
"account_deleted": "Compte supprimé !",
|
||||
"account_one": "Compte",
|
||||
"account_other": "Comptes",
|
||||
"certificate_deleted": "Certificat supprimé !",
|
||||
"certificate_one": "Certificat",
|
||||
"certificate_other": "Certificats",
|
||||
"city": "Ville",
|
||||
"common_name": "Nom commun",
|
||||
"country": "Pays",
|
||||
"global_reach": "Portée mondiale",
|
||||
"global_reach_account_id": "ID de compte ",
|
||||
"invalid_certificate": "certificat invalide",
|
||||
"invalid_key": "Clé privée invalide",
|
||||
"location_details_title": "Emplacement",
|
||||
"organization": "Organisation",
|
||||
"private_key": "Clé privée",
|
||||
"province": "province",
|
||||
"state": "Etat"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithme",
|
||||
"algorithm_other": "Algorithmes",
|
||||
@@ -1008,6 +1062,9 @@
|
||||
"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_devices_confirm": "Voulez-vous vraiment supprimer tous les appareils et leurs statistiques de la passerelle ? Cette action n'est pas réversible",
|
||||
"delete_devices_loading": "Ce processus peut prendre jusqu'à 5 minutes",
|
||||
"delete_simulation_devices": "Supprimer des appareils",
|
||||
"delete_success": "Simulation supprimée !",
|
||||
"duration": "Durée",
|
||||
"error_devices": "Périphériques d'erreur",
|
||||
@@ -1066,6 +1123,7 @@
|
||||
"title": "Les abonnés"
|
||||
},
|
||||
"system": {
|
||||
"advanced": "Avancée",
|
||||
"backend_logs": "Journaux principaux",
|
||||
"configuration": "Configuration",
|
||||
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
|
||||
|
||||
@@ -223,6 +223,7 @@
|
||||
"day": "Dia",
|
||||
"days": "Dias",
|
||||
"default": "Padrão",
|
||||
"defaults": "Predefinições",
|
||||
"description": "Descrição",
|
||||
"details": "Detalhes",
|
||||
"device_details": "Detalhes do dispositivo",
|
||||
@@ -268,6 +269,7 @@
|
||||
"map": "Mapa",
|
||||
"max": "máximo",
|
||||
"min": "minuto",
|
||||
"miscellaneous": "Diversos",
|
||||
"mode": "Modo",
|
||||
"model": "Modelo",
|
||||
"modified": "Modificado",
|
||||
@@ -702,11 +704,32 @@
|
||||
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
|
||||
},
|
||||
"firmware": {
|
||||
"confirm_default_data": "Confirme as informações abaixo e clique em 'Confirmar' quando estiver pronto para iniciar o processo",
|
||||
"create_success": "Criou novas configurações de firmware padrão!",
|
||||
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
|
||||
"default_created_error_one": "{{count}} erro ao tentar criar uma nova configuração",
|
||||
"default_created_error_other": "{{count}} erros ao tentar criar uma nova configuração",
|
||||
"default_created_one": "{{count}} configuração de firmware padrão criada",
|
||||
"default_created_other": "{{count}} configurações de firmware padrão criadas",
|
||||
"default_found_one": "Revisão válida encontrada para {{count}} tipo de dispositivo",
|
||||
"default_found_other": "Foram encontradas revisões válidas para {{count}} tipos de dispositivo",
|
||||
"default_mass_delete_success_one": "Configuração de firmware padrão {{count}} excluída!",
|
||||
"default_mass_delete_success_other": "Excluídas {{count}} configurações de firmware padrão!",
|
||||
"default_not_found_one": "Nenhuma versão de firmware válida para {{count}} tipo de dispositivo",
|
||||
"default_not_found_other": "Nenhuma versão de firmware válida para {{count}} tipos de dispositivo",
|
||||
"default_title": "",
|
||||
"default_update_success": "Firmware padrão atualizado para {{deviceType}}!",
|
||||
"delete_success": "Configuração de firmware padrão excluída!",
|
||||
"edit_default_title": "Este é o firmware atual usado como versão mínima para novos APs do tipo {{deviceType}}. Se um novo AP {{deviceType}} se conectar ao gateway, ele será atualizado automaticamente para esta versão.",
|
||||
"fetching_defaults": "Buscando todo o firmware disponível para os tipos de dispositivos selecionados...",
|
||||
"last_db_update_modal": "banco de dados de firmware",
|
||||
"last_db_update_title": "base de dados",
|
||||
"one": "Firmware",
|
||||
"select_default_device_types": "Selecione todos os tipos de dispositivos que deseja segmentar com esta nova regra de firmware padrão. Se você não conseguir encontrar o tipo de dispositivo desejado, significa que eles já têm uma regra aplicada.",
|
||||
"select_default_revision": "Agora você pode selecionar a revisão mínima para a qual deseja que seus tipos de dispositivo sejam direcionados",
|
||||
"start_db_update": "Iniciar atualização do banco de dados",
|
||||
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
|
||||
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída",
|
||||
"update_success": "Informações de firmware padrão salvas!"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Distribuído por",
|
||||
@@ -715,6 +738,7 @@
|
||||
"form": {
|
||||
"captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)",
|
||||
"certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"",
|
||||
"invalid_alphanumeric_with_dash": "Caracteres aceitos. são apenas alfanuméricos (letras e números)",
|
||||
"invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12",
|
||||
"invalid_email": "E-mail inválido",
|
||||
"invalid_file_content": "Conteúdo de arquivo inválido. Confirme se está no formato válido",
|
||||
@@ -741,7 +765,11 @@
|
||||
"invalid_static_ipv4_e": "Endereço inválido, este intervalo é reservado para experimentos (classe E). O primeiro octeto deve ser 223 ou inferior",
|
||||
"invalid_third_party": "String JSON de terceiros inválida. Confirme se seu valor é um JSON válido",
|
||||
"key_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN PRIVATE KEY-----\" e termine com \"-----END PRIVATE KEY-----\"",
|
||||
"max_length": "Comprimento máximo de {{max}} caracteres.",
|
||||
"max_value": "Valor máximo de {{max}}",
|
||||
"min_length": "Comprimento mínimo de {{min}} caracteres.",
|
||||
"min_max_string": "O valor precisa ter um comprimento entre {{min}} (inclusive) e {{max}} (inclusive)",
|
||||
"min_value": "Valor mínimo de {{min}}",
|
||||
"missing_interface_upstream": "Você precisa ter pelo menos uma interface upstream. No momento, todas as suas interfaces estão downstream",
|
||||
"new_email_to_notify": "Novo e-mail para notificar",
|
||||
"new_phone_to_notify": "Novo telefone para notificar",
|
||||
@@ -883,6 +911,11 @@
|
||||
"one": "Notificação",
|
||||
"other": "Notificações"
|
||||
},
|
||||
"openroaming": {
|
||||
"pool_strategy": "Estratégia de pool",
|
||||
"radius_endpoint_one": "Ponto final do raio",
|
||||
"radius_endpoint_other": "Pontos finais de raio"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
|
||||
"delete_operator": "Excluir operador",
|
||||
@@ -948,6 +981,27 @@
|
||||
"title": "RESTRIÇÕES",
|
||||
"tty": "Acesso TTY"
|
||||
},
|
||||
"roaming": {
|
||||
"account_created": "Nova conta criada!",
|
||||
"account_deleted": "Conta excluída!",
|
||||
"account_one": "Conta",
|
||||
"account_other": "Contas",
|
||||
"certificate_deleted": "Certificado excluído!",
|
||||
"certificate_one": "Certificado",
|
||||
"certificate_other": "Certificados",
|
||||
"city": "Cidade",
|
||||
"common_name": "Nome comum",
|
||||
"country": "País",
|
||||
"global_reach": "Alcance global",
|
||||
"global_reach_account_id": "ID da conta",
|
||||
"invalid_certificate": "Certificado inválido",
|
||||
"invalid_key": "Chave privada inválida",
|
||||
"location_details_title": "Localização",
|
||||
"organization": "Organização",
|
||||
"private_key": "Chave privada",
|
||||
"province": "província",
|
||||
"state": "Estado"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algoritmo",
|
||||
"algorithm_other": "Algoritmos",
|
||||
@@ -1008,6 +1062,9 @@
|
||||
"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_devices_confirm": "Tem certeza de que deseja remover todos os dispositivos e suas estatísticas do gateway? Esta ação não é reversível",
|
||||
"delete_devices_loading": "Este processo pode levar até 5 minutos",
|
||||
"delete_simulation_devices": "Apagar dispositivos",
|
||||
"delete_success": "Simulação excluída!",
|
||||
"duration": "Duração",
|
||||
"error_devices": "Dispositivos de Erro",
|
||||
@@ -1066,6 +1123,7 @@
|
||||
"title": "Inscritos"
|
||||
},
|
||||
"system": {
|
||||
"advanced": "Avançado",
|
||||
"backend_logs": "Registros de back-end",
|
||||
"configuration": "Configuração",
|
||||
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
|
||||
|
||||
8
src/@tanstack.react-table.d.ts
vendored
@@ -4,18 +4,22 @@ import '@tanstack/react-table';
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
ref?: React.MutableRefObject<HTMLTableCellElement | null>;
|
||||
customMinWidth?: string;
|
||||
anchored?: boolean;
|
||||
stopPropagation?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
anchored?: boolean;
|
||||
hasPopover?: boolean;
|
||||
customMaxWidth?: string;
|
||||
customMinWidth?: string;
|
||||
customWidth?: string;
|
||||
isMonospace?: boolean;
|
||||
isCentered?: boolean;
|
||||
columnSelectorOptions?: {
|
||||
label?: string;
|
||||
};
|
||||
rowContentOptions?: {
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
headerOptions?: {
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ interface Props {
|
||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||
onOpenRebootModal: (serialNumber: string) => void;
|
||||
onOpenReEnrollModal?: (serialNumber: string) => void;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isCompact?: boolean;
|
||||
}
|
||||
@@ -49,11 +50,13 @@ const DeviceActionDropdown = ({
|
||||
onOpenConfigureModal,
|
||||
onOpenScriptModal,
|
||||
onOpenRebootModal,
|
||||
onOpenReEnrollModal,
|
||||
size,
|
||||
isCompact,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const deviceType = device?.deviceType ?? 'ap';
|
||||
const connectColor = useColorModeValue('blackAlpha', 'gray');
|
||||
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
||||
const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({
|
||||
@@ -205,7 +208,7 @@ const DeviceActionDropdown = ({
|
||||
isDisabled={isDisabled}
|
||||
onClick={handleOpenScan}
|
||||
colorScheme="teal"
|
||||
hidden={isCompact}
|
||||
hidden={isCompact || deviceType !== 'ap'}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Menu>
|
||||
@@ -221,7 +224,7 @@ const DeviceActionDropdown = ({
|
||||
<Portal>
|
||||
<MenuList maxH="315px" overflowY="auto">
|
||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact}>
|
||||
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact || deviceType !== 'ap'}>
|
||||
{t('controller.configure.title')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleConnectClick} hidden={!isCompact}>
|
||||
@@ -233,13 +236,18 @@ const DeviceActionDropdown = ({
|
||||
<MenuItem onClick={handleRebootClick} hidden={!isCompact}>
|
||||
{t('commands.reboot')}
|
||||
</MenuItem>
|
||||
{onOpenReEnrollModal && (
|
||||
<MenuItem onClick={() => onOpenReEnrollModal(device.serialNumber)}>
|
||||
{t('controller.devices.re_enroll')}
|
||||
</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}>
|
||||
<MenuItem onClick={handleOpenScan} hidden={!isCompact || deviceType !== 'ap'}>
|
||||
{t('commands.wifiscan')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
||||
import { Pencil, X } from '@phosphor-icons/react';
|
||||
import { Pencil, Stop } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConfirmCloseAlertModal } from '../../Modals/ConfirmCloseAlert';
|
||||
|
||||
@@ -48,7 +48,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
|
||||
colorScheme="gray"
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
rightIcon={isEditing ? <X size={20} /> : <Pencil size={20} />}
|
||||
rightIcon={isEditing ? <Stop size={20} /> : <Pencil size={20} />}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
ml={ml}
|
||||
@@ -68,7 +68,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
|
||||
colorScheme="gray"
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
icon={isEditing ? <X size={20} /> : <Pencil size={20} />}
|
||||
icon={isEditing ? <Stop size={20} /> : <Pencil size={20} />}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isDisabled}
|
||||
ml={ml}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...p
|
||||
return (
|
||||
<Tooltip label={tooltip ?? label}>
|
||||
<Tag size="lg" colorScheme="blue" {...props}>
|
||||
<TagLeftIcon boxSize="18px" as={icon} />
|
||||
<TagLeftIcon boxSize="18px" as={icon} mt={-0.5} />
|
||||
<TagLabel>{label}</TagLabel>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
|
||||
@@ -24,7 +24,6 @@ export const DataGridCellRow = <TValue extends object>({
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
borderRight="1px solid gray"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Td
|
||||
@@ -55,6 +54,7 @@ export const DataGridCellRow = <TValue extends object>({
|
||||
: undefined
|
||||
}
|
||||
border="0.5px solid gray"
|
||||
style={cell.column.columnDef.meta?.rowContentOptions?.style}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
|
||||
@@ -8,7 +8,7 @@ export type DataGridHeaderRowProps<TValue extends object> = {
|
||||
};
|
||||
|
||||
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
|
||||
<Tr p={0} borderRight="1px solid gray">
|
||||
<Tr p={0}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Th
|
||||
color="gray.400"
|
||||
|
||||
@@ -40,13 +40,16 @@ export type DataGridOptions<TValue extends object> = {
|
||||
onRowClick?: (row: TValue) => (() => void) | undefined;
|
||||
refetch?: () => void;
|
||||
showAsCard?: boolean;
|
||||
hideTablePreferences?: boolean;
|
||||
hideTableTitleRow?: boolean;
|
||||
};
|
||||
|
||||
export type DataGridProps<TValue extends object> = {
|
||||
innerTableKey?: string | number;
|
||||
controller: UseDataGridReturn;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
header: {
|
||||
title: string;
|
||||
title: string | React.ReactNode;
|
||||
objectListed: string;
|
||||
leftContent?: React.ReactNode;
|
||||
addButton?: React.ReactNode;
|
||||
@@ -58,6 +61,7 @@ export type DataGridProps<TValue extends object> = {
|
||||
};
|
||||
|
||||
export const DataGrid = <TValue extends object>({
|
||||
innerTableKey,
|
||||
controller,
|
||||
columns,
|
||||
header,
|
||||
@@ -149,7 +153,21 @@ export const DataGrid = <TValue extends object>({
|
||||
...tableOptions,
|
||||
});
|
||||
|
||||
if (isLoading && data.length === 0) {
|
||||
// If this is a manual DataTable, with a page index that is higher than 0 and higher than the max possible page, we send to index 0
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
options.isManual &&
|
||||
!isLoading &&
|
||||
data &&
|
||||
pagination.pageIndex > 0 &&
|
||||
options.count !== undefined &&
|
||||
Math.ceil(options.count / pagination.pageSize) - 1 < pagination.pageIndex
|
||||
) {
|
||||
controller.onPaginationChange({ pageIndex: 0, pageSize: pagination.pageSize });
|
||||
}
|
||||
}, [options.count, isLoading, pagination, data]);
|
||||
|
||||
if (isLoading && !options.showAsCard && data.length === 0) {
|
||||
return (
|
||||
<Center>
|
||||
<Spinner size="xl" />
|
||||
@@ -160,25 +178,29 @@ export const DataGrid = <TValue extends object>({
|
||||
return options.showAsCard ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
{typeof header.title === 'string' ? (
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
) : (
|
||||
header.title
|
||||
)}
|
||||
{header.leftContent}
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
{options.hideTablePreferences ? null : (
|
||||
// @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">
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
@@ -206,7 +228,7 @@ export const DataGrid = <TValue extends object>({
|
||||
</Card>
|
||||
) : (
|
||||
<Box w="100%">
|
||||
<Flex mb={2}>
|
||||
<Flex mb={2} hidden={options.hideTableTitleRow}>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
@@ -215,16 +237,16 @@ export const DataGrid = <TValue extends object>({
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
{options.hideTablePreferences ? null : (
|
||||
// @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">
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px" key={innerTableKey}>
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
|
||||
@@ -2,7 +2,8 @@ import * as React from 'react';
|
||||
import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const getDefaultSettings = (settings?: string) => {
|
||||
const getDefaultSettings = ({ settings, showAllRows }: { settings?: string; showAllRows?: boolean }) => {
|
||||
if (showAllRows) return { pageSize: 1000, pageIndex: 0 };
|
||||
let limit = 10;
|
||||
let index = 0;
|
||||
|
||||
@@ -54,9 +55,10 @@ export type UseDataGridProps = {
|
||||
tableSettingsId: string;
|
||||
defaultOrder: string[];
|
||||
defaultSortBy?: SortingState;
|
||||
showAllRows?: boolean;
|
||||
};
|
||||
|
||||
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: UseDataGridProps) => {
|
||||
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder, showAllRows }: UseDataGridProps) => {
|
||||
const orderSetting = `${tableSettingsId}.order`;
|
||||
const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`;
|
||||
const pageSetting = `${tableSettingsId}.page`;
|
||||
@@ -66,8 +68,9 @@ export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: Us
|
||||
const [columnOrder, setColumnOrder] = React.useState<string[]>(
|
||||
getSavedColumnOrder(defaultOrder ?? [], tableSettingsId),
|
||||
);
|
||||
const [pageInfo, setPageInfo] = React.useState<PaginationState>(getDefaultSettings(tableSettingsId));
|
||||
|
||||
const [pageInfo, setPageInfo] = React.useState<PaginationState>(
|
||||
getDefaultSettings({ settings: tableSettingsId, showAllRows }),
|
||||
);
|
||||
const setNewColumnOrder = React.useCallback(
|
||||
(newOrder: string[]) => {
|
||||
setColumnOrder(newOrder);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FormControl, FormErrorMessage, FormLabel } from '@chakra-ui/react';
|
||||
import { Select } from 'chakra-react-select';
|
||||
import { CreatableSelect, Select } from 'chakra-react-select';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -25,6 +25,7 @@ const propTypes = {
|
||||
isHidden: PropTypes.bool,
|
||||
isPortal: PropTypes.bool.isRequired,
|
||||
definitionKey: PropTypes.string,
|
||||
isCreatable: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@@ -36,6 +37,7 @@ const defaultProps = {
|
||||
isDisabled: false,
|
||||
isHidden: false,
|
||||
definitionKey: null,
|
||||
isCreatable: false,
|
||||
};
|
||||
|
||||
const FastMultiSelectInput = ({
|
||||
@@ -50,6 +52,7 @@ const FastMultiSelectInput = ({
|
||||
isRequired,
|
||||
isDisabled,
|
||||
isHidden,
|
||||
isCreatable,
|
||||
isPortal,
|
||||
definitionKey,
|
||||
}) => {
|
||||
@@ -61,35 +64,62 @@ const FastMultiSelectInput = ({
|
||||
{label}
|
||||
<ConfigurationFieldExplanation definitionKey={definitionKey} />
|
||||
</FormLabel>
|
||||
<Select
|
||||
chakraStyles={{
|
||||
control: (provided, { isDisabled: isControlDisabled }) => ({
|
||||
...provided,
|
||||
borderRadius: '15px',
|
||||
opacity: isControlDisabled ? '0.8 !important' : '1',
|
||||
border: '2px solid',
|
||||
}),
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
backgroundColor: 'unset',
|
||||
border: 'unset',
|
||||
}),
|
||||
}}
|
||||
classNamePrefix={isPortal ? 'chakra-react-select' : ''}
|
||||
menuPortalTarget={isPortal ? document.body : undefined}
|
||||
isMulti
|
||||
closeMenuOnSelect={false}
|
||||
options={canSelectAll ? [{ value: '*', label: t('common.all') }, ...options] : options}
|
||||
value={
|
||||
value?.map((val) => {
|
||||
if (val === '*') return { value: val, label: t('common.all') };
|
||||
return options.find((opt) => opt.value === val);
|
||||
}) ?? []
|
||||
}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
{isCreatable ? (
|
||||
<CreatableSelect
|
||||
chakraStyles={{
|
||||
control: (provided, { isDisabled: isControlDisabled }) => ({
|
||||
...provided,
|
||||
borderRadius: '15px',
|
||||
opacity: isControlDisabled ? '0.8 !important' : '1',
|
||||
border: '2px solid',
|
||||
}),
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
backgroundColor: 'unset',
|
||||
border: 'unset',
|
||||
}),
|
||||
}}
|
||||
classNamePrefix={isPortal ? 'chakra-react-select' : ''}
|
||||
menuPortalTarget={isPortal ? document.body : undefined}
|
||||
isMulti
|
||||
closeMenuOnSelect={false}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
) : (
|
||||
<Select
|
||||
chakraStyles={{
|
||||
control: (provided, { isDisabled: isControlDisabled }) => ({
|
||||
...provided,
|
||||
borderRadius: '15px',
|
||||
opacity: isControlDisabled ? '0.8 !important' : '1',
|
||||
border: '2px solid',
|
||||
}),
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
backgroundColor: 'unset',
|
||||
border: 'unset',
|
||||
}),
|
||||
}}
|
||||
classNamePrefix={isPortal ? 'chakra-react-select' : ''}
|
||||
menuPortalTarget={isPortal ? document.body : undefined}
|
||||
isMulti
|
||||
closeMenuOnSelect={false}
|
||||
options={canSelectAll ? [{ value: '*', label: t('common.all') }, ...options] : options}
|
||||
value={
|
||||
value?.map((val) => {
|
||||
if (val === '*') return { value: val, label: t('common.all') };
|
||||
return options.find((opt) => opt.value === val);
|
||||
}) ?? []
|
||||
}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
<FormErrorMessage>{error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ const propTypes = {
|
||||
canSelectAll: PropTypes.bool,
|
||||
isPortal: PropTypes.bool,
|
||||
definitionKey: PropTypes.string,
|
||||
isCreatable: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@@ -31,6 +32,7 @@ const defaultProps = {
|
||||
canSelectAll: false,
|
||||
isPortal: false,
|
||||
definitionKey: null,
|
||||
isCreatable: false,
|
||||
};
|
||||
|
||||
const MultiSelectField = ({
|
||||
@@ -43,25 +45,39 @@ const MultiSelectField = ({
|
||||
emptyIsUndefined,
|
||||
canSelectAll,
|
||||
hasVirtualAll,
|
||||
isCreatable,
|
||||
isPortal,
|
||||
definitionKey,
|
||||
}) => {
|
||||
const [{ value }, { touched, error }, { setValue, setTouched }] = useField(name);
|
||||
|
||||
const onChange = useCallback((option) => {
|
||||
const allIndex = option.findIndex((opt) => opt.value === '*');
|
||||
if (option.length === 0 && emptyIsUndefined) {
|
||||
setValue(undefined);
|
||||
} else if (allIndex === 0 && option.length > 1) {
|
||||
const newValues = option.slice(1);
|
||||
setValue(newValues.map((val) => val.value));
|
||||
} else if (allIndex >= 0) {
|
||||
if (!hasVirtualAll) setValue(['*']);
|
||||
else setValue(options.map(({ value: v }) => v));
|
||||
} else if (option.length > 0) setValue(option.map((val) => val.value));
|
||||
else setValue([]);
|
||||
setTouched(true);
|
||||
}, []);
|
||||
const onChange = useCallback(
|
||||
(option) => {
|
||||
if (isCreatable) {
|
||||
if (typeof option === 'string') {
|
||||
setValue([...value, option]);
|
||||
} else {
|
||||
setValue(option);
|
||||
}
|
||||
|
||||
// setValue([...value, option]);
|
||||
} else {
|
||||
const allIndex = option.findIndex((opt) => opt.value === '*');
|
||||
if (option.length === 0 && emptyIsUndefined) {
|
||||
setValue(undefined);
|
||||
} else if (allIndex === 0 && option.length > 1) {
|
||||
const newValues = option.slice(1);
|
||||
setValue(newValues.map((val) => val.value));
|
||||
} else if (allIndex >= 0) {
|
||||
if (!hasVirtualAll) setValue(['*']);
|
||||
else setValue(options.map(({ value: v }) => v));
|
||||
} else if (option.length > 0) setValue(option.map((val) => val.value));
|
||||
else setValue([]);
|
||||
setTouched(true);
|
||||
}
|
||||
},
|
||||
[value],
|
||||
);
|
||||
|
||||
const onFieldBlur = useCallback(() => {
|
||||
setTouched(true);
|
||||
@@ -82,6 +98,7 @@ const MultiSelectField = ({
|
||||
isHidden={isHidden}
|
||||
isPortal={isPortal}
|
||||
definitionKey={definitionKey}
|
||||
isCreatable={isCreatable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,11 @@ const chakraStyles: (
|
||||
placeholder: (provided) => ({
|
||||
...provided,
|
||||
lineHeight: '1',
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
}),
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
@@ -31,6 +36,10 @@ const chakraStyles: (
|
||||
backgroundColor: colorMode === 'light' ? 'white' : 'gray.600',
|
||||
borderRadius: '15px',
|
||||
}),
|
||||
input: (provided) => ({
|
||||
...provided,
|
||||
gridArea: '1 / 2 / 4 / 4 !important',
|
||||
}),
|
||||
});
|
||||
|
||||
interface SearchOption extends OptionBase {
|
||||
@@ -95,18 +104,43 @@ const GlobalSearchBar = () => {
|
||||
.then(() => callback([]));
|
||||
}
|
||||
if (v.match('^[a-fA-F0-9-*]+$')) {
|
||||
let result: { label: string; value: string; type: 'serial' }[] = [];
|
||||
let tryAgain = true;
|
||||
|
||||
await store
|
||||
.searchSerialNumber(v)
|
||||
.then((res) => {
|
||||
callback(
|
||||
res.map((r) => ({
|
||||
result = res.map((r) => ({
|
||||
label: r,
|
||||
value: r,
|
||||
type: 'serial',
|
||||
}));
|
||||
tryAgain = false;
|
||||
})
|
||||
.catch(() => {
|
||||
result = [];
|
||||
});
|
||||
|
||||
if (tryAgain) {
|
||||
// Wait 1 second and try again
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
await store
|
||||
.searchSerialNumber(v)
|
||||
.then((res) => {
|
||||
result = res.map((r) => ({
|
||||
label: r,
|
||||
value: r,
|
||||
type: 'serial',
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => []);
|
||||
}));
|
||||
tryAgain = false;
|
||||
})
|
||||
.catch(() => {
|
||||
result = [];
|
||||
});
|
||||
}
|
||||
|
||||
callback(result);
|
||||
}
|
||||
return callback([]);
|
||||
},
|
||||
|
||||
257
src/components/Modals/CableDiagnosticsModal/index.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Text,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
Center,
|
||||
Spinner,
|
||||
Checkbox,
|
||||
Stack,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
} from '@chakra-ui/react';
|
||||
import { PlugsConnected } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
||||
import { ResponsiveButton } from 'components/Buttons/ResponsiveButton';
|
||||
import { ModalHeader } from 'components/Containers/Modal/ModalHeader';
|
||||
import { useCableDiagnostics } from 'hooks/Network/Devices';
|
||||
import { ModalProps } from 'models/Modal';
|
||||
import Button from 'theme/components/button';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
|
||||
export type CableDiagnosticsModalProps = {
|
||||
modalProps: ModalProps;
|
||||
serialNumber: string;
|
||||
port: string;
|
||||
};
|
||||
|
||||
type DiagnosticsRow = {
|
||||
port: string;
|
||||
linkStatus: string;
|
||||
pairA: string;
|
||||
pairB: string;
|
||||
pairC: string;
|
||||
pairD: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
type OpticalRow = {
|
||||
port: string;
|
||||
vendorName: string;
|
||||
formFactor: string;
|
||||
partNumber: string;
|
||||
serialNumber: string;
|
||||
temperature: string;
|
||||
txPower: string;
|
||||
rxPower: string;
|
||||
revision: string;
|
||||
};
|
||||
|
||||
export const CableDiagnosticsModal = ({
|
||||
modalProps: { isOpen, onClose },
|
||||
serialNumber,
|
||||
port,
|
||||
}: CableDiagnosticsModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedPorts, setSelectedPorts] = React.useState<string[]>([]);
|
||||
const [diagnosticsResult, setDiagnosticsResult] = React.useState<any>(null);
|
||||
const { mutateAsync: diagnose, isLoading } = useCableDiagnostics({ serialNumber });
|
||||
|
||||
const handlePortToggle = (port: string) => {
|
||||
setSelectedPorts((prev) => (prev.includes(port) ? prev.filter((p) => p !== port) : [...prev, port]));
|
||||
};
|
||||
|
||||
const handleDiagnose = async () => {
|
||||
if (port) {
|
||||
try {
|
||||
const result = await diagnose([port]);
|
||||
setDiagnosticsResult(result);
|
||||
} catch (error) {
|
||||
console.error('Error diagnosing cable:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'cable.diagnostics.table',
|
||||
defaultOrder: ['port', 'linkStatus', 'pairA', 'pairB', 'pairC', 'pairD', 'type'],
|
||||
showAllRows: true,
|
||||
});
|
||||
|
||||
const columns: DataGridColumn<DiagnosticsRow | OpticalRow>[] = React.useMemo(() => {
|
||||
const data = diagnosticsResult?.results?.status?.text?.[port];
|
||||
const isOpticalData = data && 'form-factor' in data;
|
||||
|
||||
return isOpticalData
|
||||
? [
|
||||
{
|
||||
id: 'vendorName',
|
||||
header: 'Vendor Name',
|
||||
accessorKey: 'vendorName',
|
||||
},
|
||||
{
|
||||
id: 'formFactor',
|
||||
header: 'Form Factor',
|
||||
accessorKey: 'formFactor',
|
||||
},
|
||||
{
|
||||
id: 'partNumber',
|
||||
header: 'Part Number',
|
||||
accessorKey: 'partNumber',
|
||||
},
|
||||
{
|
||||
id: 'serialNumber',
|
||||
header: 'Serial Number',
|
||||
accessorKey: 'serialNumber',
|
||||
},
|
||||
{
|
||||
id: 'temperature',
|
||||
header: 'Temperature',
|
||||
accessorKey: 'temperature',
|
||||
},
|
||||
{
|
||||
id: 'txPower',
|
||||
header: 'TX Power',
|
||||
accessorKey: 'txPower',
|
||||
},
|
||||
{
|
||||
id: 'rxPower',
|
||||
header: 'RX Power',
|
||||
accessorKey: 'rxPower',
|
||||
},
|
||||
{
|
||||
id: 'revision',
|
||||
header: 'Revision',
|
||||
accessorKey: 'revision',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
id: 'port',
|
||||
header: 'Port',
|
||||
accessorKey: 'port',
|
||||
},
|
||||
{
|
||||
id: 'linkStatus',
|
||||
header: 'Link Status',
|
||||
accessorKey: 'linkStatus',
|
||||
},
|
||||
{
|
||||
id: 'pairA',
|
||||
header: 'Pair A',
|
||||
accessorKey: 'pairA',
|
||||
},
|
||||
{
|
||||
id: 'pairB',
|
||||
header: 'Pair B',
|
||||
accessorKey: 'pairB',
|
||||
},
|
||||
{
|
||||
id: 'pairC',
|
||||
header: 'Pair C',
|
||||
accessorKey: 'pairC',
|
||||
},
|
||||
{
|
||||
id: 'pairD',
|
||||
header: 'Pair D',
|
||||
accessorKey: 'pairD',
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
accessorKey: 'type',
|
||||
},
|
||||
];
|
||||
}, [diagnosticsResult]);
|
||||
|
||||
const formatDiagnosticsData = (result: any): (DiagnosticsRow | OpticalRow)[] => {
|
||||
if (!result?.results?.status?.text?.[port]) return [];
|
||||
|
||||
const data = result.results.status.text[port];
|
||||
|
||||
if (data['form-factor']) {
|
||||
return [
|
||||
{
|
||||
port,
|
||||
vendorName: data['vendor-name'] || 'N/A',
|
||||
formFactor: data['form-factor'] || 'N/A',
|
||||
partNumber: data['part-number'] || 'N/A',
|
||||
serialNumber: data['serial-number'] || 'N/A',
|
||||
temperature: data.temperature ? `${data.temperature.toFixed(2)}` : 'N/A',
|
||||
txPower: data['tx-optical-power'] ? `${data['tx-optical-power']}` : 'N/A',
|
||||
rxPower: data['rx-optical-power'] ? `${data['rx-optical-power']}` : 'N/A',
|
||||
revision: data.revision || 'N/A',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
port,
|
||||
linkStatus: data['link-status'],
|
||||
pairA: `${data['pair-A'].meters} (${data['pair-A'].status})`,
|
||||
pairB: `${data['pair-B'].meters} (${data['pair-B'].status})`,
|
||||
pairC: `${data['pair-C'].meters} (${data['pair-C'].status})`,
|
||||
pairD: `${data['pair-D'].meters} (${data['pair-D'].status})`,
|
||||
type: data.type,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW="50vw">
|
||||
<ModalHeader title={t('commands.cable_diagnostics')} right={<CloseButton onClick={onClose} />} />
|
||||
<ModalBody pb={6}>
|
||||
{isLoading ? (
|
||||
<Center my={4} flexDirection="column" gap={4}>
|
||||
<Spinner size="lg" />
|
||||
<Text>Please wait...</Text>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
Please do not close this window. This may take a few seconds.
|
||||
</Text>
|
||||
</Center>
|
||||
) : (
|
||||
<Center flexDirection="column" gap={4}>
|
||||
<ResponsiveButton
|
||||
color="blue"
|
||||
icon={<PlugsConnected size={20} />}
|
||||
label={`${
|
||||
diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 ? 'Retake' : 'Start'
|
||||
} Test for Port ${port}`}
|
||||
onClick={handleDiagnose}
|
||||
isLoading={isLoading}
|
||||
isDisabled={!port}
|
||||
isCompact={false}
|
||||
/>
|
||||
{diagnosticsResult && formatDiagnosticsData(diagnosticsResult).length > 0 && (
|
||||
<DataGrid<DiagnosticsRow | OpticalRow>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: '',
|
||||
objectListed: 'Cable Diagnostics',
|
||||
}}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
data={formatDiagnosticsData(diagnosticsResult)}
|
||||
options={{
|
||||
isHidingControls: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Center>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export type ConfigureModalProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => {
|
||||
const _ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const configure = useConfigureDevice({ serialNumber });
|
||||
@@ -45,6 +45,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
const onImportConfiguration = () => {
|
||||
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
|
||||
};
|
||||
|
||||
const isValid = React.useMemo(() => {
|
||||
try {
|
||||
JSON.parse(newConfig);
|
||||
@@ -58,22 +59,59 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
try {
|
||||
const config = JSON.parse(newConfig);
|
||||
configure.mutate(config, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `configure-success-${serialNumber}`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.configure.success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onSuccess: (data) => {
|
||||
if (data.errorCode === 0) {
|
||||
toast({
|
||||
id: `configure-success-${serialNumber}`,
|
||||
title: t('common.success'),
|
||||
description:
|
||||
data.status === 'pending'
|
||||
? 'Command is pending! It will execute once the device connects'
|
||||
: t('controller.configure.success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
modalProps.onClose();
|
||||
} else if (data.errorCode === 1) {
|
||||
toast({
|
||||
id: `configure-warning-${serialNumber}`,
|
||||
title: 'Warning',
|
||||
description: `${data?.errorText ?? 'Unknown Warning'}`,
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
modalProps.onClose();
|
||||
} else {
|
||||
toast({
|
||||
id: `config-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
modalProps.onClose();
|
||||
},
|
||||
});
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (modalProps.isOpen) {
|
||||
getDevice.refetch();
|
||||
} else {
|
||||
setNewConfig('');
|
||||
}
|
||||
}, [modalProps.isOpen]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...modalProps}
|
||||
@@ -124,3 +162,5 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigureModal = React.memo(_ConfigureModal);
|
||||
|
||||
50
src/components/Modals/ReEnrollModal/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Center, Spinner, Alert, Button } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '../Modal';
|
||||
import { useReEnroll } from 'hooks/Network/ReEnroll';
|
||||
import { ModalProps } from 'models/Modal';
|
||||
|
||||
interface Props {
|
||||
modalProps: ModalProps;
|
||||
serialNumber: string;
|
||||
}
|
||||
|
||||
const ReEnrollModal = ({ modalProps: { isOpen, onClose }, serialNumber }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate: reEnroll, isLoading } = useReEnroll({ serialNumber });
|
||||
|
||||
const submit = () => {
|
||||
reEnroll(
|
||||
{ serialNumber, when: 0 },
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={t('controller.devices.re_enroll')}>
|
||||
{isLoading ? (
|
||||
<Center>
|
||||
<Spinner size="lg" />
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<Alert colorScheme="blue" mb={6}>
|
||||
{t('controller.devices.re_enroll_warning', { serialNumber })}
|
||||
</Alert>
|
||||
<Center mb={6}>
|
||||
<Button size="lg" colorScheme="blue" onClick={submit} fontWeight="bold">
|
||||
{t('controller.devices.confirm_re_enroll', { serialNumber })}
|
||||
</Button>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReEnrollModal;
|
||||
@@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '../Modal';
|
||||
import { lowercaseFirstLetter } from 'helpers/stringHelper';
|
||||
import { useTelemetry } from 'hooks/Network/Telemetry';
|
||||
import { secondsDuration } from 'helpers/dateFormatting';
|
||||
|
||||
export type TelemetryModalProps = {
|
||||
serialNumber: string;
|
||||
@@ -146,8 +147,7 @@ const _TelemetryModal = ({ serialNumber, modalProps }: TelemetryModalProps) => {
|
||||
{t('controller.telemetry.interval')}: {form.interval} {lowercaseFirstLetter(t('common.seconds'))}
|
||||
</p>
|
||||
<p>
|
||||
{t('controller.telemetry.duration')}: {form.interval}{' '}
|
||||
{lowercaseFirstLetter(t('controller.telemetry.minutes'))}
|
||||
{t('controller.telemetry.duration')}: {secondsDuration(form.lifetime, t)}
|
||||
</p>
|
||||
<p>
|
||||
{t('controller.telemetry.types')}: {form.types.join(', ')}
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Heading,
|
||||
IconButton,
|
||||
Spacer,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react';
|
||||
import { Box, Button, Center, Heading, IconButton, Spacer, useColorMode } from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,21 +7,124 @@ import { v4 as uuid } from 'uuid';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { ScanChannel } from 'models/Device';
|
||||
import { DeviceScanResult, ScanChannel } from 'models/Device';
|
||||
import { DataGrid } from 'components/DataTables/DataGrid';
|
||||
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
|
||||
|
||||
interface Props {
|
||||
channelInfo: ScanChannel;
|
||||
}
|
||||
const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
|
||||
|
||||
const ueCell = (ies: DeviceScanResult['ies'], setIes: (ies: DeviceScanResult['ies']) => void) => (
|
||||
<Button size="sm" colorScheme="blue" onClick={() => setIes(ies)} w="100%">
|
||||
{ies.length}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const centerIfUndefinedCell = (v?: string | number, suffix?: string) =>
|
||||
v !== undefined ? `${v}${suffix ? `${suffix}` : ''}` : <Center>-</Center>;
|
||||
|
||||
const ResultCard = ({ channelInfo: { channel, devices } }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const [ies, setIes] = React.useState<{ content: unknown; name: string; type: number }[] | undefined>();
|
||||
const tableController = useDataGrid({
|
||||
tableSettingsId: 'wifiscan.devices.table',
|
||||
defaultOrder: ['ssid', 'signal', 'actions'],
|
||||
defaultSortBy: [
|
||||
{
|
||||
desc: false,
|
||||
id: 'ssid',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const columns: DataGridColumn<DeviceScanResult>[] = React.useMemo(
|
||||
(): DataGridColumn<DeviceScanResult>[] => [
|
||||
{
|
||||
id: 'ssid',
|
||||
header: 'SSID',
|
||||
footer: '',
|
||||
accessorKey: 'ssid',
|
||||
meta: {
|
||||
anchored: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'signal',
|
||||
header: 'Signal',
|
||||
footer: '',
|
||||
accessorKey: 'signal',
|
||||
cell: (v) => `${v.cell.row.original.signal} db`,
|
||||
meta: {
|
||||
anchored: true,
|
||||
customWidth: '80px',
|
||||
alwaysShow: true,
|
||||
rowContentOptions: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'station',
|
||||
header: 'UEs',
|
||||
accessorKey: 'sta_count',
|
||||
cell: (v) => centerIfUndefinedCell(v.cell.row.original.sta_count),
|
||||
meta: {
|
||||
anchored: true,
|
||||
customWidth: '40px',
|
||||
alwaysShow: true,
|
||||
rowContentOptions: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'utilization',
|
||||
header: 'Ch. Util.',
|
||||
accessorKey: 'ch_util',
|
||||
cell: (v) => centerIfUndefinedCell(v.cell.row.original.ch_util, '%'),
|
||||
meta: {
|
||||
anchored: true,
|
||||
customWidth: '60px',
|
||||
alwaysShow: true,
|
||||
headerOptions: {
|
||||
tooltip: 'Channel Utilization (%)',
|
||||
},
|
||||
rowContentOptions: {
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'ies',
|
||||
header: 'Ies',
|
||||
footer: '',
|
||||
accessorKey: 'actions',
|
||||
cell: (v) => ueCell(v.cell.row.original.ies ?? [], setIes),
|
||||
meta: {
|
||||
customWidth: '50px',
|
||||
isCentered: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card variant="widget">
|
||||
<Card>
|
||||
<CardHeader display="flex">
|
||||
<Heading size="md" my="auto">
|
||||
{t('commands.channel')} #{channel} ({devices.length} {t('devices.title')})
|
||||
{t('commands.channel')} #{channel} ({devices.length}{' '}
|
||||
{devices.length === 1 ? t('devices.one') : t('devices.title')})
|
||||
</Heading>
|
||||
<Spacer />
|
||||
{ies && (
|
||||
@@ -49,52 +139,43 @@ const ResultCard: React.FC<Props> = ({ channelInfo: { channel, devices } }) => {
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box h="400px" w="100%" overflowY="auto" overflowX="auto" px={0}>
|
||||
{ies ? (
|
||||
<Box w="800px">
|
||||
{ies.map(({ content, name, type }) => (
|
||||
<Box key={uuid()} my={2}>
|
||||
<Heading size="sm" mb={2} textDecor="underline">
|
||||
{name} ({type})
|
||||
</Heading>
|
||||
<JsonViewer
|
||||
rootName={false}
|
||||
displayDataTypes={false}
|
||||
enableClipboard
|
||||
theme={colorMode === 'light' ? undefined : 'dark'}
|
||||
value={content as object}
|
||||
style={{ background: 'unset', display: 'unset' }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Table variant="simple" px={0}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>SSID</Th>
|
||||
<Th width="110px" isNumeric>
|
||||
{t('commands.signal')}
|
||||
</Th>
|
||||
<Th w="10px">IEs</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{devices.map((dev) => (
|
||||
<Tr key={uuid()}>
|
||||
<Td>{dev.ssid}</Td>
|
||||
<Td width="110px">{dev.signal} db</Td>
|
||||
<Td w="10px">
|
||||
<Button size="sm" colorScheme="blue" onClick={() => setIes(dev.ies ?? [])}>
|
||||
{dev.ies?.length ?? 0}
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</Box>
|
||||
{ies ? (
|
||||
<Box w="800px">
|
||||
{ies.map(({ content, name, type }) => (
|
||||
<Box key={uuid()} my={2}>
|
||||
<Heading size="sm" mb={2} textDecor="underline">
|
||||
{name} ({type})
|
||||
</Heading>
|
||||
<JsonViewer
|
||||
rootName={false}
|
||||
displayDataTypes={false}
|
||||
enableClipboard
|
||||
theme={colorMode === 'light' ? undefined : 'dark'}
|
||||
value={content as object}
|
||||
style={{ background: 'unset', display: 'unset' }}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<DataGrid<DeviceScanResult>
|
||||
controller={tableController}
|
||||
header={{
|
||||
title: '',
|
||||
objectListed: t('devices.title'),
|
||||
}}
|
||||
columns={columns}
|
||||
data={devices}
|
||||
options={{
|
||||
count: devices.length,
|
||||
onRowClick: (device) => () => setIes(device.ies ?? []),
|
||||
hideTablePreferences: true,
|
||||
isHidingControls: true,
|
||||
minimumHeight: '0px',
|
||||
hideTableTitleRow: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Alert, Heading, SimpleGrid } from '@chakra-ui/react';
|
||||
import { Alert, Heading, VStack } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ResultCard from './ResultCard';
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
setCsvData: (data: DeviceScanResult[]) => void;
|
||||
}
|
||||
|
||||
const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
|
||||
const WifiScanResultDisplay = ({ results, setCsvData }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const scanResults = useMemo(() => {
|
||||
@@ -54,18 +54,18 @@ const WifiScanResultDisplay: React.FC<Props> = ({ results, setCsvData }) => {
|
||||
return (
|
||||
<>
|
||||
{results.errorCode === 1 && (
|
||||
<Heading size="sm">
|
||||
<Heading size="md">
|
||||
<Alert colorScheme="red">{t('commands.wifiscan_error_1')}</Alert>
|
||||
</Heading>
|
||||
)}
|
||||
<Heading size="sm">
|
||||
<Heading size="md" mb={2}>
|
||||
{t('commands.execution_time')}: {Math.floor(results.executionTime / 1000)}s
|
||||
</Heading>
|
||||
<SimpleGrid minChildWidth="360px" spacing={2}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{scanResults?.scanList.map((channel) => (
|
||||
<ResultCard key={uuid()} channelInfo={channel} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
1
src/custom.d.ts
vendored
@@ -8,3 +8,4 @@ declare module '*.png' {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
||||
@@ -174,12 +174,37 @@ export const useGetEventQueue = () => {
|
||||
};
|
||||
|
||||
const configureDevice = (serialNumber: string) => async (configuration: Record<string, unknown>) =>
|
||||
axiosGw.post<unknown>(`device/${serialNumber}/configure`, {
|
||||
when: 0,
|
||||
UUID: 1,
|
||||
serialNumber,
|
||||
configuration,
|
||||
});
|
||||
axiosGw
|
||||
.post<unknown>(`device/${serialNumber}/configure`, {
|
||||
when: 0,
|
||||
UUID: 1,
|
||||
serialNumber,
|
||||
configuration,
|
||||
})
|
||||
.then(
|
||||
(res) =>
|
||||
res.data as Partial<{
|
||||
UUID: string;
|
||||
attachFile: number;
|
||||
command: string;
|
||||
completed: number;
|
||||
custom: number;
|
||||
deferred: boolean;
|
||||
details: Record<string, unknown>;
|
||||
errorCode: number;
|
||||
errorText: string;
|
||||
executed: number;
|
||||
executionTime: number;
|
||||
lastTry: number;
|
||||
results: Record<string, unknown>;
|
||||
serialNumber: string;
|
||||
status: string;
|
||||
submitted: number;
|
||||
submittedBy: string;
|
||||
waitingForFile: number;
|
||||
when: number;
|
||||
}>,
|
||||
);
|
||||
|
||||
export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -187,6 +212,8 @@ export const useConfigureDevice = ({ serialNumber }: { serialNumber: string }) =
|
||||
return useMutation(configureDevice(serialNumber), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
queryClient.invalidateQueries(['device', serialNumber]);
|
||||
queryClient.invalidateQueries(['devices']);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -248,27 +275,14 @@ const startScript = (data: { serialNumber: string; timeout?: number; [k: string]
|
||||
})
|
||||
.then((response: { data: DeviceCommandHistory }) => response.data);
|
||||
export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(startScript, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
},
|
||||
onError: (e) => {
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: 'script-error',
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,10 +3,12 @@ import { axiosGw } from 'constants/axiosInstances';
|
||||
import { useEndpointStatus } from 'hooks/useEndpointStatus';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { DeviceConfiguration } from 'models/Device';
|
||||
import { DevicePlatform } from './Devices';
|
||||
|
||||
export type DefaultConfigurationResponse = {
|
||||
configuration: DeviceConfiguration;
|
||||
created: number;
|
||||
platform: DevicePlatform;
|
||||
description: string;
|
||||
lastModified: number;
|
||||
modelIds: string[];
|
||||
|
||||
103
src/hooks/Network/DefaultFirmware.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { AtLeast } from 'models/General';
|
||||
|
||||
export type DefaultFirmware = {
|
||||
deviceType: string;
|
||||
description: string;
|
||||
uri: string;
|
||||
revision: string;
|
||||
imageCreationDate: number;
|
||||
created: number;
|
||||
lastModified: number;
|
||||
};
|
||||
|
||||
export type DefaultFirmwareResponse = {
|
||||
firmwares: DefaultFirmware[];
|
||||
};
|
||||
|
||||
const getDefaultFirmware = async () =>
|
||||
axiosGw.get('default_firmwares').then((response) => response.data as DefaultFirmwareResponse);
|
||||
|
||||
export const useGetDefaultFirmware = () =>
|
||||
useQuery(['default_firmwares', 'all'], getDefaultFirmware, {
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
|
||||
const getDefaultFirmwareByDeviceType = async (context: QueryFunctionContext<[string, string]>) =>
|
||||
axiosGw.get(`default_firmware/${context.queryKey[1]}`).then((response) => response.data as DefaultFirmware);
|
||||
|
||||
export const useGetDefaultFirmwareByDeviceType = (deviceType: string) =>
|
||||
useQuery(['default_firmwares', deviceType], getDefaultFirmwareByDeviceType);
|
||||
|
||||
export const createDefaultFirmware = async (defaultFirmware: DefaultFirmware) =>
|
||||
axiosGw
|
||||
.post(`default_firmware/${defaultFirmware.deviceType}`, defaultFirmware)
|
||||
.then((response) => response.data as DefaultFirmware);
|
||||
|
||||
export const useCreateDefaultFirmware = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(createDefaultFirmware, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['default_firmwares']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDefaultFirmware = async (deviceType: string) =>
|
||||
axiosGw.delete(`default_firmware/${deviceType}`).then((response) => response.data as DefaultFirmware);
|
||||
|
||||
export const useDeleteDefaultFirmware = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteDefaultFirmware, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['default_firmwares']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteBatchDefaultFirmware = async (deviceTypes: string[]) => {
|
||||
const promises = deviceTypes.map((deviceType) =>
|
||||
axiosGw
|
||||
.delete(`default_firmware/${deviceType}`)
|
||||
.then(() => ({
|
||||
deviceType,
|
||||
success: true,
|
||||
}))
|
||||
.catch((e) => ({
|
||||
deviceType,
|
||||
error: axios.isAxiosError(e) ? e.response?.data.ErrorDescription : e,
|
||||
})),
|
||||
);
|
||||
const res = await Promise.allSettled(promises);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const useDeleteBatchDefaultFirmware = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteBatchDefaultFirmware, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['default_firmwares']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const updateDefaultFirmware = async (defaultFirmware: AtLeast<DefaultFirmware, 'deviceType'>) =>
|
||||
axiosGw
|
||||
.put(`default_firmware/${defaultFirmware.deviceType}`, defaultFirmware)
|
||||
.then((response) => response.data as DefaultFirmware);
|
||||
|
||||
export const useUpdateDefaultFirmware = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(updateDefaultFirmware, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['default_firmwares']);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -9,15 +9,21 @@ import { AxiosError } from 'models/Axios';
|
||||
import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device';
|
||||
import { Note } from 'models/Note';
|
||||
import { PageInfo } from 'models/Table';
|
||||
import { DeviceCommandHistory } from './Commands';
|
||||
|
||||
const getDeviceCount = () =>
|
||||
axiosGw.get('devices?countOnly=true').then((response) => response.data) as Promise<{ count: number }>;
|
||||
export const DEVICE_PLATFORMS = ['all', 'ap', 'switch'] as const;
|
||||
export type DevicePlatform = (typeof DEVICE_PLATFORMS)[number];
|
||||
|
||||
export const useGetDeviceCount = ({ enabled }: { enabled: boolean }) => {
|
||||
const getDeviceCount = (platform: DevicePlatform) =>
|
||||
axiosGw.get(`devices?countOnly=true&platform=${platform}`).then((response) => response.data) as Promise<{
|
||||
count: number;
|
||||
}>;
|
||||
|
||||
export const useGetDeviceCount = ({ enabled, platform = 'all' }: { enabled: boolean; platform?: DevicePlatform }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['devices', 'count'], getDeviceCount, {
|
||||
return useQuery(['devices', 'count', { platform }], () => getDeviceCount(platform), {
|
||||
enabled,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('inventory-fetching-error'))
|
||||
@@ -42,13 +48,14 @@ export type DeviceWithStatus = {
|
||||
associations_2G: number;
|
||||
associations_5G: number;
|
||||
associations_6G: number;
|
||||
blackListed?: boolean;
|
||||
compatible: string;
|
||||
connected: boolean;
|
||||
connectReason?: string;
|
||||
certificateExpiryDate?: number;
|
||||
createdTimestamp: number;
|
||||
devicePassword: string;
|
||||
deviceType: 'AP' | 'SWITCH' | 'IOT' | 'MESH';
|
||||
deviceType: 'ap' | 'switch';
|
||||
entity: string;
|
||||
firmware: string;
|
||||
fwUpdatePolicy: string;
|
||||
@@ -95,25 +102,27 @@ export const getSingleDeviceWithStatus = (serialNumber: string) =>
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
const getDevices = (limit: number, offset: number) =>
|
||||
const getDevices = (limit: number, offset: number, platform: DevicePlatform) =>
|
||||
axiosGw
|
||||
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`)
|
||||
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}&platform=${platform}`)
|
||||
.then((response) => response.data) as Promise<{ devicesWithStatus: DeviceWithStatus[] }>;
|
||||
|
||||
export const useGetDevices = ({
|
||||
pageInfo,
|
||||
enabled,
|
||||
onError,
|
||||
platform = 'all',
|
||||
}: {
|
||||
pageInfo?: PageInfo;
|
||||
enabled: boolean;
|
||||
onError?: (e: AxiosError) => void;
|
||||
platform?: DevicePlatform;
|
||||
}) => {
|
||||
const offset = pageInfo?.limit !== undefined ? pageInfo.limit * pageInfo.index : 0;
|
||||
|
||||
return useQuery(
|
||||
['devices', 'all', { limit: pageInfo?.limit, offset }],
|
||||
() => getDevices(pageInfo?.limit || 0, offset),
|
||||
['devices', 'all', { limit: pageInfo?.limit, offset, platform }],
|
||||
() => getDevices(pageInfo?.limit || 0, offset, platform),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: enabled && pageInfo !== undefined,
|
||||
@@ -123,22 +132,28 @@ export const useGetDevices = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getAllDevices = async () => {
|
||||
const getAllDevices = async (platform: DevicePlatform) => {
|
||||
let offset = 0;
|
||||
let devices: DeviceWithStatus[] = [];
|
||||
let devicesResponse: { devicesWithStatus: DeviceWithStatus[] };
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
devicesResponse = await getDevices(500, offset);
|
||||
devicesResponse = await getDevices(500, offset, platform);
|
||||
devices = devices.concat(devicesResponse.devicesWithStatus);
|
||||
offset += 500;
|
||||
} while (devicesResponse.devicesWithStatus.length === 500);
|
||||
return devices;
|
||||
};
|
||||
|
||||
export const useGetAllDevicesWithStatus = ({ onError }: { onError?: (e: AxiosError) => void }) => {
|
||||
export const useGetAllDevicesWithStatus = ({
|
||||
onError,
|
||||
platform = 'all',
|
||||
}: {
|
||||
onError?: (e: AxiosError) => void;
|
||||
platform?: DevicePlatform;
|
||||
}) => {
|
||||
const { isReady } = useEndpointStatus('owgw');
|
||||
return useQuery(['devices', 'all', 'full'], getAllDevices, {
|
||||
return useQuery(['devices', 'all', 'full', { platform }], () => getAllDevices(platform), {
|
||||
enabled: isReady && false,
|
||||
onError,
|
||||
});
|
||||
@@ -151,6 +166,7 @@ export type DeviceStatus = {
|
||||
connected: boolean;
|
||||
connectReason?: string;
|
||||
certificateExpiryDate: number;
|
||||
certificateIssuerName?: string;
|
||||
connectionCompletionTime: number;
|
||||
firmware: string;
|
||||
ipAddress: string;
|
||||
@@ -362,6 +378,40 @@ export const useWifiScanDevice = ({ serialNumber }: { serialNumber: string }) =>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCableDiagnostics = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation(
|
||||
(ports: string[]): Promise<unknown> =>
|
||||
axiosGw
|
||||
.post(`device/${serialNumber}/cable-diagnostics`, {
|
||||
serial: serialNumber,
|
||||
ports,
|
||||
when: 0,
|
||||
})
|
||||
.then(({ data }) => data),
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
console.log('Success data: ', data);
|
||||
},
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('commands.cablediagnostics_error', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetDeviceRtty = ({ serialNumber, extraId }: { serialNumber: string; extraId: string | number }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
@@ -431,3 +481,45 @@ export const useUpdateDevice = ({ serialNumber }: { serialNumber: string }) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDeviceBatch = async (pattern: string) => {
|
||||
if (pattern.length < 6) throw new Error('Pattern must be at least 6 characters long');
|
||||
|
||||
axiosGw.delete(`devices?macPattern=${pattern}`);
|
||||
};
|
||||
|
||||
export const useDeleteDeviceBatch = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteDeviceBatch, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['devices']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type PowerCyclePort = {
|
||||
/** Ex.: Ethernet0 */
|
||||
name: string;
|
||||
/** Cycle length in MS. Default is 10 000 */
|
||||
cycle?: number;
|
||||
};
|
||||
|
||||
export type PowerCycleRequest = {
|
||||
serial: string;
|
||||
when: number;
|
||||
ports: PowerCyclePort[];
|
||||
};
|
||||
|
||||
export const usePowerCycle = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(
|
||||
(request: PowerCycleRequest) =>
|
||||
axiosGw.post(`device/${request.serial}/powercycle`, request).then((res) => res.data as DeviceCommandHistory),
|
||||
{
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries(['commands']);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offs
|
||||
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
|
||||
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
|
||||
|
||||
const getAllAvailableFirmware = async (deviceType: string) => {
|
||||
export const getAllAvailableFirmware = async (deviceType: string) => {
|
||||
const limit = 500;
|
||||
let offset = 0;
|
||||
let data: { firmwares: Firmware[] } = { firmwares: [] };
|
||||
@@ -70,25 +70,66 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe
|
||||
|
||||
return useMutation(
|
||||
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
|
||||
axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
|
||||
serialNumber,
|
||||
when: 0,
|
||||
keepRedirector,
|
||||
uri,
|
||||
signature,
|
||||
}),
|
||||
axiosGw
|
||||
.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
|
||||
serialNumber,
|
||||
when: 0,
|
||||
keepRedirector,
|
||||
uri,
|
||||
signature,
|
||||
})
|
||||
.then(
|
||||
(response) =>
|
||||
response as {
|
||||
data: {
|
||||
errorCode: number;
|
||||
errorText: string;
|
||||
status: string;
|
||||
results?: {
|
||||
status?: {
|
||||
error?: number;
|
||||
resultCode?: number;
|
||||
text?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
},
|
||||
),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `device-upgrade-success-${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('commands.firmware_upgrade_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
onSuccess: ({ data }) => {
|
||||
if (data.errorCode === 0) {
|
||||
toast({
|
||||
id: `device-upgrade-success-${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('commands.firmware_upgrade_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
} else if (data.errorCode === 1) {
|
||||
toast({
|
||||
id: `device-upgrade-warning-${uuid()}`,
|
||||
title: 'Warning',
|
||||
description: `${data?.errorText ?? 'Unknown Warning'}`,
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
} else {
|
||||
toast({
|
||||
id: `device-upgrade-error-${uuid()}`,
|
||||
title: t('common.error'),
|
||||
description: `${data?.errorText ?? 'Unknown Error'} (Code ${data.errorCode})`,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
|
||||
78
src/hooks/Network/ReEnroll.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
|
||||
export type ReEnrollRequest = {
|
||||
serialNumber: string;
|
||||
when?: number;
|
||||
};
|
||||
|
||||
export type ReEnrollResponse = {
|
||||
UUID: string;
|
||||
command: 're-enroll' | 'reenroll';
|
||||
completed: number;
|
||||
custom: number;
|
||||
details: {
|
||||
serial: string;
|
||||
when: number;
|
||||
};
|
||||
errorCode: number;
|
||||
errorText: string;
|
||||
executed: number;
|
||||
executionTime: number;
|
||||
results: {
|
||||
serial: string;
|
||||
status: {
|
||||
error: number;
|
||||
resultCode: number;
|
||||
resultText: string;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
serialNumber: string;
|
||||
status: string;
|
||||
submitted: number;
|
||||
submittedBy: string;
|
||||
when: number;
|
||||
};
|
||||
|
||||
const reEnrollDevice = async ({ serialNumber, when = 0 }: ReEnrollRequest) =>
|
||||
axiosGw.post<ReEnrollResponse>(`device/${serialNumber}/reenroll`, {
|
||||
serial: serialNumber,
|
||||
when,
|
||||
});
|
||||
|
||||
export const useReEnroll = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useMutation(reEnrollDevice, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||
queryClient.invalidateQueries(['device', serialNumber]);
|
||||
queryClient.invalidateQueries(['device-status', serialNumber]);
|
||||
toast({
|
||||
id: `re-enroll-success-${serialNumber}`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.devices.re_enroll_initiated', { serialNumber }),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
id: `re-enroll-error-${serialNumber}`,
|
||||
title: t('common.error'),
|
||||
description: error?.response?.data?.ErrorDescription || t('common.error'),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
173
src/hooks/Network/Simulations.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { axiosGw, axiosOwls } from 'constants/axiosInstances';
|
||||
import { AtLeast } from 'models/General';
|
||||
|
||||
export type Simulation = {
|
||||
clientInterval: number;
|
||||
concurrentDeviceS: number;
|
||||
deviceType: string;
|
||||
devices: number;
|
||||
gateway: string;
|
||||
healthCheckInterval: number;
|
||||
id: string;
|
||||
keepAlive: number;
|
||||
key: string;
|
||||
macPrefix: string;
|
||||
minAssociations: number;
|
||||
maxAssociations: number;
|
||||
minClients: number;
|
||||
maxClients: number;
|
||||
name: string;
|
||||
reconnectionInterval: number;
|
||||
simulationLength: number;
|
||||
stateInterval: number;
|
||||
threads: number;
|
||||
};
|
||||
|
||||
const getSimulations = () => async () =>
|
||||
axiosOwls.get(`simulation/*`).then((response) => response.data as { list: Simulation[] });
|
||||
|
||||
export const useGetSimulations = () =>
|
||||
useQuery(['simulations'], getSimulations(), {
|
||||
keepPreviousData: true,
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
const getSimulation = (id?: string) => async () =>
|
||||
axiosOwls.get(`simulation/${id}`).then((response) => response.data as { list: Simulation[] });
|
||||
export const useGetSimulation = ({ id }: { id?: string }) =>
|
||||
useQuery(['simulation', id], getSimulation(id), {
|
||||
keepPreviousData: true,
|
||||
enabled: id !== undefined,
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
const createSimulation = async (newSimulation: Partial<Simulation>) => axiosOwls.post(`simulation/0`, newSimulation);
|
||||
export const useCreateSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(createSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulations']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateSimulation = async (newSimulation: AtLeast<Simulation, 'id'>) =>
|
||||
axiosOwls.put(`simulation/${newSimulation.id}`, newSimulation).then((response) => response.data as Simulation);
|
||||
export const useUpdateSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(updateSimulation, {
|
||||
onSuccess: (newSimulation) => {
|
||||
queryClient.setQueryData(['simulation'], newSimulation);
|
||||
queryClient.invalidateQueries(['simulations']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSimulation = async ({ id }: { id: string }) => axiosOwls.delete(`simulation/${id}`);
|
||||
export const useDeleteSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulations']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const startSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation/${id}?operation=start`);
|
||||
export const useStartSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(startSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulations', 'status']);
|
||||
},
|
||||
});
|
||||
};
|
||||
const stopSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) =>
|
||||
axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=stop`);
|
||||
export const useStopSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(stopSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulations', 'status']);
|
||||
},
|
||||
});
|
||||
};
|
||||
const cancelSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) =>
|
||||
axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=cancel`);
|
||||
export const useCancelSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(cancelSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulations', 'status']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type SimulationStatus = {
|
||||
endTime: number;
|
||||
errorDevices: number;
|
||||
id: string;
|
||||
liveDevices: number;
|
||||
msgsRx: number;
|
||||
msgsTx: number;
|
||||
owner: string;
|
||||
rx: number;
|
||||
simulationId: string;
|
||||
startTime: number;
|
||||
state: 'running' | 'completed' | 'cancelled' | 'none';
|
||||
timeToFullDevices: number;
|
||||
tx: number;
|
||||
};
|
||||
const getSimulationsStatus = async () =>
|
||||
axiosOwls.get(`status/*`).then((response) => response.data as SimulationStatus[]);
|
||||
export const useGetSimulationsStatus = () =>
|
||||
useQuery(['simulations', 'status'], getSimulationsStatus, {
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const getSimulationStatus = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||
axiosOwls.get(`status/${context.queryKey[2]}`).then((response) => response.data as SimulationStatus);
|
||||
export const useGetSimulationStatus = ({ id }: { id: string }) =>
|
||||
useQuery(['simulations', 'status', id], getSimulationStatus, {
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const getSimulationHistory = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||
axiosOwls.get(`results/${context.queryKey[2]}`).then((response) => response.data.list as SimulationStatus[]);
|
||||
export const useGetSimulationHistory = ({ id }: { id: string }) =>
|
||||
useQuery(['simulations', 'history', id], getSimulationHistory, {
|
||||
keepPreviousData: true,
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const deleteSimulationResult = async ({ id }: { id: string }) => axiosOwls.delete(`results/${id}`);
|
||||
export const useDeleteSimulationResult = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteSimulationResult, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulations', 'history']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSimulatedDevices = async () => axiosGw.delete('devices?simulatedDevices=true');
|
||||
|
||||
export const useDeleteSimulatedDevices = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteSimulatedDevices, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['devices']);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -2,7 +2,24 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
type DeviceInterfaceStatistics = {
|
||||
export type DeviceLinkState = {
|
||||
carrier?: number;
|
||||
counters?: {
|
||||
collisions: number;
|
||||
multicast: number;
|
||||
rx_bytes: number;
|
||||
rx_dropped: number;
|
||||
rx_errors: number;
|
||||
rx_packets: number;
|
||||
tx_bytes: number;
|
||||
tx_dropped: number;
|
||||
tx_errors: number;
|
||||
tx_packets: number;
|
||||
};
|
||||
duplex?: string;
|
||||
speed?: number;
|
||||
};
|
||||
export type DeviceInterfaceStatistics = {
|
||||
clients: {
|
||||
ipv4_addresses?: string[];
|
||||
ipv6_addresses?: string[];
|
||||
@@ -42,6 +59,7 @@ type DeviceInterfaceStatistics = {
|
||||
dynamic_vlan?: number;
|
||||
inactive: number;
|
||||
ipaddr_v4: string;
|
||||
fingerprint?: object;
|
||||
rssi: number;
|
||||
rx_bytes: number;
|
||||
rx_duration: number;
|
||||
@@ -112,11 +130,21 @@ export type DeviceStatistics = {
|
||||
channel: number;
|
||||
band?: string[];
|
||||
channel_width: string;
|
||||
noise: number;
|
||||
noise?: number;
|
||||
phy: string;
|
||||
receive_ms: number;
|
||||
transmit_ms: number;
|
||||
temperature?: number;
|
||||
tx_power: number;
|
||||
frequency?: number[];
|
||||
survey?: {
|
||||
busy: number;
|
||||
frequency: number;
|
||||
noise: number;
|
||||
time: number;
|
||||
time_rx: number;
|
||||
time_tx: number;
|
||||
}[];
|
||||
}[];
|
||||
dynamic_vlans?: {
|
||||
vid: number;
|
||||
@@ -138,18 +166,10 @@ export type DeviceStatistics = {
|
||||
};
|
||||
'link-state'?: {
|
||||
downstream: {
|
||||
eth1?: {
|
||||
carrier?: number;
|
||||
duplex?: string;
|
||||
speed?: number;
|
||||
};
|
||||
[key: string]: DeviceLinkState;
|
||||
};
|
||||
upstream: {
|
||||
eth0?: {
|
||||
carrier?: number;
|
||||
duplex?: string;
|
||||
speed?: number;
|
||||
};
|
||||
[key: string]: DeviceLinkState;
|
||||
};
|
||||
};
|
||||
'lldp-peers'?: {
|
||||
@@ -190,6 +210,7 @@ export const useGetDeviceLastStats = ({
|
||||
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
staleTime: 1000 * 60,
|
||||
refetchInterval: 1000 * 60,
|
||||
onError,
|
||||
});
|
||||
|
||||
|
||||
@@ -199,3 +199,36 @@ export const useUpdateSystemLogLevels = ({ endpoint, token }: { endpoint: string
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type SystemResources = {
|
||||
currRealMem: number;
|
||||
currVirtMem: number;
|
||||
numberOfFileDescriptors: number;
|
||||
peakRealMem: number;
|
||||
peakVirtMem: number;
|
||||
};
|
||||
|
||||
export const useGetSystemResources = ({
|
||||
endpoint,
|
||||
token,
|
||||
onSuccess,
|
||||
}: {
|
||||
endpoint: string;
|
||||
token: string;
|
||||
onSuccess?: (data: SystemResources) => void;
|
||||
}) =>
|
||||
useQuery(
|
||||
['systemResources', endpoint],
|
||||
() =>
|
||||
axiosInstance
|
||||
.get(`${endpoint}/api/v1/system?command=resources`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then(({ data }: { data: SystemResources }) => data),
|
||||
{
|
||||
refetchInterval: 5 * 1000,
|
||||
onSuccess,
|
||||
},
|
||||
);
|
||||
|
||||
80
src/hooks/useNotification.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as React from 'react';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { isApiError } from 'models/Axios';
|
||||
|
||||
export type SuccessNotificationProps = {
|
||||
description: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type ApiErrorNotificationProps = {
|
||||
e: unknown;
|
||||
fallbackMessage?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const useNotification = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
const successToast = ({ description, id }: SuccessNotificationProps) => {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.success'),
|
||||
description,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
};
|
||||
|
||||
const apiErrorToast = ({ e, id, fallbackMessage }: ApiErrorNotificationProps) => {
|
||||
if (isApiError(e)) {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.error'),
|
||||
description: e.response?.data.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.error'),
|
||||
description: fallbackMessage,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const errorToast = ({ description, id }: SuccessNotificationProps) => {
|
||||
toast({
|
||||
id: id ?? uuid(),
|
||||
title: t('common.error'),
|
||||
description,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
};
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
successToast,
|
||||
errorToast,
|
||||
apiErrorToast,
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
};
|
||||
|
||||
export type UseNotificationReturn = ReturnType<typeof useNotification>;
|
||||
@@ -1,16 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
CircularProgressLabel,
|
||||
Flex,
|
||||
Heading,
|
||||
Icon,
|
||||
Text,
|
||||
Tooltip,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowSquareDown, ArrowSquareUp, Clock } from '@phosphor-icons/react';
|
||||
import { Flex, Heading, Icon, Text, Tooltip, VStack } from '@chakra-ui/react';
|
||||
import { ArrowSquareDown, ArrowSquareUp } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
||||
@@ -20,74 +10,26 @@ import { useGetDevicesStats } from 'hooks/Network/Devices';
|
||||
const SidebarDevices = () => {
|
||||
const { t } = useTranslation();
|
||||
const getStats = useGetDevicesStats({});
|
||||
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
||||
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
||||
|
||||
const time = React.useMemo(() => {
|
||||
if (lastTime === undefined || lastUpdate === undefined) return null;
|
||||
|
||||
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
||||
|
||||
return Math.max(0, Math.floor(seconds / 1000));
|
||||
}, [lastTime, lastUpdate]);
|
||||
|
||||
const circleColor = () => {
|
||||
if (time === null) return 'gray.300';
|
||||
if (time < 10) return 'green.300';
|
||||
if (time < 30) return 'yellow.300';
|
||||
return 'red.300';
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setLastUpdate(new Date());
|
||||
}, [getStats.data]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setLastTime(new Date());
|
||||
}, 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!getStats.data) return null;
|
||||
|
||||
return (
|
||||
<Card p={4}>
|
||||
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||
<CircularProgress
|
||||
isIndeterminate
|
||||
color={circleColor()}
|
||||
position="absolute"
|
||||
right="6px"
|
||||
top="6px"
|
||||
w="unset"
|
||||
size={6}
|
||||
thickness="14px"
|
||||
>
|
||||
<CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel>
|
||||
</CircularProgress>
|
||||
</Tooltip>
|
||||
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||
<Box position="absolute" right="8px" top="8px" w="unset" hidden>
|
||||
<Clock size={16} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<VStack mb={-1}>
|
||||
<Flex flexDir="column" textAlign="center">
|
||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||
<Heading size="xs" display="flex" justifyContent="center">
|
||||
<Text>
|
||||
{t('common.connected')} {t('devices.title')}{' '}
|
||||
</Text>{' '}
|
||||
</Heading>
|
||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||
<Heading size="md" textAlign="center" mt={1}>
|
||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||
</Heading>
|
||||
</Tooltip>
|
||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||
|
||||
<Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
|
||||
<Tooltip hasArrow label="Rx">
|
||||
<Flex alignItems="center" mr={1}>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
Portal,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowCircleLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -27,11 +28,20 @@ export type NavbarProps = {
|
||||
toggleSidebar: () => void;
|
||||
activeRoute?: string;
|
||||
languageSwitcher?: React.ReactNode;
|
||||
favoritesButton?: React.ReactNode;
|
||||
rightElements?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarProps) => {
|
||||
export const Navbar = ({
|
||||
toggleSidebar,
|
||||
activeRoute,
|
||||
languageSwitcher,
|
||||
favoritesButton,
|
||||
rightElements = null,
|
||||
}: NavbarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const menuProps = useDisclosure();
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const breakpoint = useBreakpoint();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
@@ -76,6 +86,20 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
|
||||
window.addEventListener('scroll', changeNavbar);
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
const onMouseEnter = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
menuProps.onOpen();
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => menuProps.onClose(), 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Flex
|
||||
@@ -99,6 +123,7 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
top="15px"
|
||||
border={scrolled ? '0.5px solid' : undefined}
|
||||
w={isCompact ? '100%' : 'calc(100% - 254px)'}
|
||||
zIndex={10}
|
||||
>
|
||||
<Flex
|
||||
w="100%"
|
||||
@@ -109,11 +134,14 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
justifyContent="center"
|
||||
>
|
||||
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
||||
<Heading size="lg">{activeRoute}</Heading>
|
||||
{activeRoute && activeRoute.length > 0 ? (
|
||||
<Heading size="lg" mr={4}>
|
||||
{activeRoute}
|
||||
</Heading>
|
||||
) : null}
|
||||
<Tooltip label={t('common.go_back')}>
|
||||
<IconButton
|
||||
mt={1}
|
||||
ml={4}
|
||||
colorScheme="blue"
|
||||
aria-label={t('common.go_back')}
|
||||
onClick={goBack}
|
||||
@@ -123,6 +151,8 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
</Tooltip>
|
||||
<Box ms="auto" w={{ base: 'unset' }}>
|
||||
<Flex alignItems="center" flexDirection="row">
|
||||
{rightElements}
|
||||
{favoritesButton}
|
||||
<Tooltip hasArrow label={t('common.theme')}>
|
||||
<IconButton
|
||||
aria-label={t('common.theme')}
|
||||
@@ -132,27 +162,33 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
/>
|
||||
</Tooltip>
|
||||
{languageSwitcher}
|
||||
<HStack spacing={{ base: '0', md: '6' }} ml={1} mr={4}>
|
||||
<Menu>
|
||||
<MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: 'none' }}>
|
||||
<Box ml={1} mr={4}>
|
||||
<Menu {...menuProps} gutter={0}>
|
||||
<MenuButton
|
||||
py={2}
|
||||
transition="all 0.3s"
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<HStack>
|
||||
{!isCompact && <Text fontWeight="bold">{user?.name}</Text>}
|
||||
<Avatar h="40px" w="40px" fontSize="0.8rem" lineHeight="2rem" src={avatar} name={user?.name} />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<Portal>
|
||||
<MenuList
|
||||
bg={useColorModeValue('white', 'gray.900')}
|
||||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
>
|
||||
<MenuItem onClick={goToProfile} w="100%">
|
||||
{t('account.title')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
<MenuList
|
||||
bg={useColorModeValue('white', 'gray.900')}
|
||||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<MenuItem onClick={goToProfile} w="100%">
|
||||
{t('account.title')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -89,7 +89,7 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
||||
</Box>
|
||||
</>
|
||||
),
|
||||
[user?.userRole, location],
|
||||
[user?.userRole, location, topNav],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { AxiosError as Err } from 'axios';
|
||||
import { AxiosError as Err, isAxiosError } from 'axios';
|
||||
|
||||
export type AxiosError = Err<{ ErrorDescription: string; ErrorCode: number }>;
|
||||
|
||||
export const isApiError = (e: unknown): e is AxiosError =>
|
||||
isAxiosError(e) && (e as AxiosError).response?.data?.ErrorDescription !== undefined;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Note } from './Note';
|
||||
|
||||
export interface GatewayDevice {
|
||||
UUID: number;
|
||||
blackListed?: boolean;
|
||||
certificateExpiryDate: number;
|
||||
compatible: string;
|
||||
configuration: unknown;
|
||||
createdTimestamp: number;
|
||||
@@ -16,6 +18,7 @@ export interface GatewayDevice {
|
||||
lastConfigurationChange: number;
|
||||
lastConfigurationDownload: number;
|
||||
lastFWUpdate: number;
|
||||
lastRecordedContact: number;
|
||||
locale: string;
|
||||
location: string;
|
||||
macAddress: string;
|
||||
@@ -112,12 +115,16 @@ interface BssidResult {
|
||||
bssid: string;
|
||||
capability: number;
|
||||
channel: number;
|
||||
/** Channel Utilization percentage (ex.: 28 -> 28% channel utilization) */
|
||||
ch_util?: number;
|
||||
frequency: number;
|
||||
ht_oper: string;
|
||||
ies: { content: unknown; name: string; type: number }[];
|
||||
last_seen: number;
|
||||
ssid: string;
|
||||
signal: number;
|
||||
/** Station count */
|
||||
sta_count?: number;
|
||||
tsf: number;
|
||||
meshid?: string;
|
||||
vht_oper: string;
|
||||
@@ -142,20 +149,8 @@ export interface WifiScanResult {
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeviceScanResult {
|
||||
bssid: string;
|
||||
capability: number;
|
||||
channel: number;
|
||||
frequency: number;
|
||||
ht_oper: string;
|
||||
ies: { content: unknown; name: string; type: number }[];
|
||||
last_seen: number;
|
||||
ssid: string;
|
||||
signal: number | string;
|
||||
tsf: number;
|
||||
meshid?: string;
|
||||
vht_oper: string;
|
||||
}
|
||||
export type DeviceScanResult = BssidResult;
|
||||
|
||||
export interface ScanChannel {
|
||||
channel: number;
|
||||
devices: DeviceScanResult[];
|
||||
|
||||
93
src/pages/AdvancedSystemPage/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { DeleteButton } from 'components/Buttons/DeleteButton';
|
||||
import { useNotification } from 'hooks/useNotification';
|
||||
import { useDeleteSimulatedDevices } from 'hooks/Network/Simulations';
|
||||
|
||||
const AdvancedSystemPage = () => {
|
||||
const { successToast, apiErrorToast } = useNotification();
|
||||
const deleteSimulatedDevices = useDeleteSimulatedDevices();
|
||||
|
||||
const handleDeleteSimulatedDevices = async () =>
|
||||
deleteSimulatedDevices.mutateAsync(undefined, {
|
||||
onSuccess: () => {
|
||||
successToast({
|
||||
id: 'delete-simulated-devices',
|
||||
description: 'Simulated devices deleted!',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
apiErrorToast({
|
||||
id: 'delete-simulated-devices',
|
||||
e,
|
||||
fallbackMessage: 'Error deleting simulated devices',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md">Operations</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box>
|
||||
<Heading size="sm">Delete Simulated Devices</Heading>
|
||||
<Text fontStyle="italic">Delete all simulated devices from the database. This action cannot be undone.</Text>
|
||||
<Popover>
|
||||
{({ onClose }) => (
|
||||
<>
|
||||
<PopoverTrigger>
|
||||
<Button colorScheme="red" rightIcon={<Trash size={20} />}>
|
||||
Delete
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>Confirm</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Text>Are you sure you want to delete all simulated devices?</Text>
|
||||
<Center mt={4}>
|
||||
<Button onClick={onClose} mr={1}>
|
||||
Cancel
|
||||
</Button>
|
||||
<DeleteButton
|
||||
ml={1}
|
||||
isLoading={deleteSimulatedDevices.isLoading}
|
||||
onClick={async () => {
|
||||
await handleDeleteSimulatedDevices();
|
||||
onClose();
|
||||
}}
|
||||
isCompact={false}
|
||||
/>
|
||||
</Center>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedSystemPage;
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, SimpleGrid, useBoolean, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { Box, Flex, useBoolean, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -16,6 +16,7 @@ import { useGetDeviceTypes } from 'hooks/Network/Firmware';
|
||||
import { useFormModal } from 'hooks/useFormModal';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
|
||||
const CreateDefaultConfigurationModal = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -68,42 +69,63 @@ const CreateDefaultConfigurationModal = () => {
|
||||
key={formKey}
|
||||
validationSchema={DefaultConfigurationSchema(t)}
|
||||
onSubmit={(data, { setSubmitting, resetForm }) => {
|
||||
createConfig.mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `config-create-success`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.configurations.create_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
modalProps.onClose();
|
||||
createConfig.mutateAsync(
|
||||
{ ...data, modelIds: data.modelIds.map((v) => v.value) },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `config-create-success`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.configurations.create_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
modalProps.onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
const e = error as AxiosError;
|
||||
toast({
|
||||
id: `config-create-error`,
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
},
|
||||
onError: (error) => {
|
||||
const e = error as AxiosError;
|
||||
toast({
|
||||
id: `config-create-error`,
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
});
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<SimpleGrid spacing={4} minChildWidth="200px">
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={isDisabled} />
|
||||
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} />
|
||||
</SimpleGrid>
|
||||
<Flex mb={4}>
|
||||
<StringField
|
||||
name="name"
|
||||
label={t('common.name')}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
maxW="340px"
|
||||
mr={4}
|
||||
/>
|
||||
<SelectField
|
||||
name="platform"
|
||||
label="Platform"
|
||||
options={[
|
||||
{ label: 'AP', value: 'ap' },
|
||||
{ label: 'Switch', value: 'switch' },
|
||||
]}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
w="max-content"
|
||||
/>
|
||||
</Flex>
|
||||
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} mb={4} />
|
||||
<MultiSelectField
|
||||
name="modelIds"
|
||||
label={t('controller.dashboard.device_types')}
|
||||
@@ -114,9 +136,10 @@ const CreateDefaultConfigurationModal = () => {
|
||||
value: devType,
|
||||
})) ?? []
|
||||
}
|
||||
isCreatable
|
||||
isRequired
|
||||
/>
|
||||
<StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} />
|
||||
<StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} mt={4} />
|
||||
</Box>
|
||||
</Formik>
|
||||
</Box>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box, SimpleGrid, useBoolean, UseDisclosureReturn, useToast } from '@chakra-ui/react';
|
||||
import { Box, Flex, useBoolean, UseDisclosureReturn, useToast } from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -15,6 +15,7 @@ import { useGetDeviceTypes } from 'hooks/Network/Firmware';
|
||||
import { useFormModal } from 'hooks/useFormModal';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
|
||||
type Props = {
|
||||
modalProps: UseDisclosureReturn;
|
||||
@@ -69,47 +70,69 @@ const EditDefaultConfiguration = ({ modalProps, config }: Props) => {
|
||||
innerRef={formRef as React.Ref<FormikProps<DefaultConfigurationResponse>>}
|
||||
initialValues={{
|
||||
...config,
|
||||
modelIds: config.modelIds.map((v) => ({ label: v, value: v })),
|
||||
configuration: JSON.stringify(config.configuration, null, 2),
|
||||
}}
|
||||
key={formKey}
|
||||
validationSchema={DefaultConfigurationSchema(t)}
|
||||
onSubmit={(data, { setSubmitting, resetForm }) => {
|
||||
updateConfig.mutateAsync(data, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `config-edit-success`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.configurations.update_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
modalProps.onClose();
|
||||
updateConfig.mutateAsync(
|
||||
{ ...data, modelIds: data.modelIds.map((v) => v.value) },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `config-edit-success`,
|
||||
title: t('common.success'),
|
||||
description: t('controller.configurations.update_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
modalProps.onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
const e = error as AxiosError;
|
||||
toast({
|
||||
id: `config-edit-error`,
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
},
|
||||
onError: (error) => {
|
||||
const e = error as AxiosError;
|
||||
toast({
|
||||
id: `config-edit-error`,
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
});
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<SimpleGrid spacing={4} minChildWidth="200px">
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={isDisabled} />
|
||||
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} />
|
||||
</SimpleGrid>
|
||||
<Flex mb={4}>
|
||||
<StringField
|
||||
name="name"
|
||||
label={t('common.name')}
|
||||
isRequired
|
||||
isDisabled={isDisabled}
|
||||
maxW="340px"
|
||||
mr={4}
|
||||
/>
|
||||
<SelectField
|
||||
name="platform"
|
||||
label="Platform"
|
||||
options={[
|
||||
{ label: 'AP', value: 'ap' },
|
||||
{ label: 'Switch', value: 'switch' },
|
||||
]}
|
||||
isRequired
|
||||
isDisabled
|
||||
w="max-content"
|
||||
/>
|
||||
</Flex>
|
||||
<StringField name="description" label={t('common.description')} isDisabled={isDisabled} mb={4} />
|
||||
<MultiSelectField
|
||||
name="modelIds"
|
||||
label={t('controller.dashboard.device_types')}
|
||||
@@ -120,9 +143,16 @@ const EditDefaultConfiguration = ({ modalProps, config }: Props) => {
|
||||
value: devType,
|
||||
})) ?? []
|
||||
}
|
||||
isCreatable
|
||||
isRequired
|
||||
/>
|
||||
<StringField name="configuration" label={t('configurations.one')} isArea isDisabled={isDisabled} />
|
||||
<StringField
|
||||
name="configuration"
|
||||
label={t('configurations.one')}
|
||||
isArea
|
||||
isDisabled={isDisabled}
|
||||
mt={4}
|
||||
/>
|
||||
</Box>
|
||||
</Formik>
|
||||
)}
|
||||
|
||||
@@ -58,6 +58,14 @@ const DefaultConfigurationsList = () => {
|
||||
Cell: ({ cell }) => dateCell(cell.row.original.lastModified),
|
||||
customWidth: '50px',
|
||||
},
|
||||
{
|
||||
id: 'platform',
|
||||
Header: 'Platform',
|
||||
Footer: '',
|
||||
accessor: 'platform',
|
||||
Cell: ({ cell }) => cell.row.original.platform.toUpperCase(),
|
||||
customWidth: '50px',
|
||||
},
|
||||
{
|
||||
id: 'modelIds',
|
||||
Header: t('controller.dashboard.device_types'),
|
||||
|
||||
@@ -6,7 +6,8 @@ export const DefaultConfigurationSchema = (t: (str: string) => string) =>
|
||||
.shape({
|
||||
name: Yup.string().required(t('form.required')),
|
||||
description: Yup.string(),
|
||||
modelIds: Yup.array().of(Yup.string()).required(t('form.required')).min(1, t('form.required')),
|
||||
modelIds: Yup.array().of(Yup.object()).required(t('form.required')).min(1, t('form.required')),
|
||||
platform: Yup.string().oneOf(['ap', 'switch']).required(t('form.required')),
|
||||
configuration: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test('configuration', t('form.invalid_json'), (v) => testJson(v ?? '')),
|
||||
@@ -15,5 +16,6 @@ export const DefaultConfigurationSchema = (t: (str: string) => string) =>
|
||||
name: '',
|
||||
description: '',
|
||||
modelIds: [],
|
||||
platform: 'ap',
|
||||
configuration: '',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
import * as React from 'react';
|
||||
import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Center,
|
||||
Flex,
|
||||
Heading,
|
||||
Spinner,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { compactDate } from 'helpers/dateFormatting';
|
||||
import { getRevision } from 'helpers/stringHelper';
|
||||
import { DefaultFirmware } from 'hooks/Network/DefaultFirmware';
|
||||
import { getAllAvailableFirmware } from 'hooks/Network/Firmware';
|
||||
|
||||
const getDefaultFirmware = async (deviceType: string, revision: string) =>
|
||||
getAllAvailableFirmware(deviceType)
|
||||
.then((res) => {
|
||||
const found = res.firmwares.find((firmware) => firmware.revision === revision);
|
||||
if (!found) {
|
||||
return { error: { deviceType, error: 'Could not find available firmware for this revision' } };
|
||||
}
|
||||
return {
|
||||
success: {
|
||||
deviceType,
|
||||
description: '',
|
||||
revision,
|
||||
imageCreationDate: found.imageDate,
|
||||
uri: found.uri,
|
||||
created: 0,
|
||||
lastModified: 0,
|
||||
},
|
||||
};
|
||||
})
|
||||
.catch(() => ({ error: { deviceType, error: 'Could not fetch firmware available for this device type' } }));
|
||||
|
||||
const getComputedData = async (deviceTypes: string[], revision: string) => {
|
||||
const defaultFirmwares: DefaultFirmware[] = [];
|
||||
const errors: { deviceType: string; error: string }[] = [];
|
||||
|
||||
const promises = deviceTypes.map((deviceType) => getDefaultFirmware(deviceType, revision));
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
results.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
if (result.value.error) {
|
||||
errors.push(result.value.error);
|
||||
} else if (result.value.success) {
|
||||
defaultFirmwares.push(result.value.success);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { defaultFirmwares, errors };
|
||||
};
|
||||
|
||||
type Props = {
|
||||
deviceTypes: string[];
|
||||
revision: string;
|
||||
goNext: (defaultFirmwares: DefaultFirmware[]) => Promise<void>;
|
||||
setNextCallback: React.Dispatch<React.SetStateAction<(() => (Promise<void> | void) | undefined) | undefined>>;
|
||||
};
|
||||
|
||||
const ConfirmDefaultFirmwareCreation = ({ deviceTypes, revision, goNext, setNextCallback }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const getCompleteData = useQuery(
|
||||
['default_firmware', 'computed_data'],
|
||||
() => getComputedData(deviceTypes, revision),
|
||||
{
|
||||
refetchOnMount: true,
|
||||
},
|
||||
);
|
||||
|
||||
const onNext = async () => {
|
||||
if (getCompleteData.data) {
|
||||
await goNext(getCompleteData.data.defaultFirmwares);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (getCompleteData.data) {
|
||||
setNextCallback(() => onNext);
|
||||
} else {
|
||||
setNextCallback(undefined);
|
||||
}
|
||||
}, [getCompleteData.data, setNextCallback]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading mb={4} size="sm">
|
||||
{t('firmware.confirm_default_data')}
|
||||
</Heading>
|
||||
{getCompleteData.isFetching ? (
|
||||
<Center my={4} display="flex" flexDirection="column">
|
||||
<Spinner size="xl" />
|
||||
<Heading size="sm" mt={2}>
|
||||
{t('firmware.fetching_defaults')}
|
||||
</Heading>
|
||||
</Center>
|
||||
) : null}
|
||||
{getCompleteData.data && !getCompleteData.isFetching ? (
|
||||
<>
|
||||
<Heading size="sm" mb={2}>
|
||||
{t('firmware.one')}: {getRevision(revision)}
|
||||
</Heading>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
<CheckCircleIcon color="green.500" mr={2} mt={-0.5} />
|
||||
{t('firmware.default_found', { count: getCompleteData.data?.defaultFirmwares.length })}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
{getCompleteData.data?.defaultFirmwares.map((data) => (
|
||||
<AccordionItem key={data.deviceType}>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
{data.deviceType}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Flex>
|
||||
<Text>Image Date: </Text>
|
||||
<Text ml={2}>{compactDate(data.imageCreationDate)}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Text>URI: </Text>
|
||||
<Text ml={2}>{data.uri}</Text>
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
<WarningIcon color="red.500" mr={2} mt={-0.5} />
|
||||
{t('firmware.default_not_found', { count: getCompleteData.data?.errors.length })} (cannot create
|
||||
valid default firmware)
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
{getCompleteData.data?.errors.map((error) => (
|
||||
<AccordionItem key={error.deviceType}>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
{error.deviceType}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>{error.error}</AccordionPanel>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmDefaultFirmwareCreation;
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { CheckCircleIcon, WarningIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
List,
|
||||
ListItem,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CreateDefaultFirmwareResult } from './utils';
|
||||
|
||||
type Props = {
|
||||
results: CreateDefaultFirmwareResult[];
|
||||
};
|
||||
|
||||
const CreateDefaultFirmwareResults = ({ results }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const successes = results.filter((result) => !result.error);
|
||||
const errors = results.filter((result) => result.error);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading size="sm">{t('controller.devices.results')}: </Heading>
|
||||
<Accordion allowToggle allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
<CheckCircleIcon color="green.500" mr={2} mt={-0.5} />
|
||||
{t('firmware.default_created', { count: successes.length })}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<List>
|
||||
{successes.map((success) => (
|
||||
<ListItem key={success.deviceType}>
|
||||
<Text>{success.deviceType}</Text>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
<WarningIcon color="red.500" mr={2} mt={-0.5} />
|
||||
{t('firmware.default_created_error_other', { count: errors.length })}
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
<List>
|
||||
{errors.map((error) => (
|
||||
<ListItem key={error.deviceType}>
|
||||
<Flex>
|
||||
<Text>{error.deviceType} -</Text>
|
||||
<Text ml={2}>{error.error}</Text>
|
||||
</Flex>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDefaultFirmwareResults;
|
||||