Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61cfa9391f | ||
|
|
39fbae1304 | ||
|
|
f5677695d8 | ||
|
|
e036419975 | ||
|
|
df72853118 | ||
|
|
f55e8cfe7e | ||
|
|
9ae67c751c | ||
|
|
9e23b51dd0 | ||
|
|
39158b0d1e | ||
|
|
acc264534e | ||
|
|
48dcb4acbf | ||
|
|
1c40f9eb4c | ||
|
|
9e418eb423 | ||
|
|
e4ff3a87a7 | ||
|
|
e82551c97f | ||
|
|
d877a4aecf | ||
|
|
5c50a40bdb | ||
|
|
9caf0f375c | ||
|
|
1ed8285452 | ||
|
|
16170e613c | ||
|
|
b3fb45dd36 | ||
|
|
54f5912da6 | ||
|
|
8542ded488 | ||
|
|
e6561faf8c | ||
|
|
f291b7b0fc | ||
|
|
d9ea2abf1a | ||
|
|
60a072809b | ||
|
|
9828d6457d | ||
|
|
d6f390d7d4 | ||
|
|
6ddafc8de0 | ||
|
|
ed5c83cf66 | ||
|
|
bf227b5e6f | ||
|
|
8bc1350e2e | ||
|
|
3aa0dd2f51 | ||
|
|
6c1f1e1db7 | ||
|
|
0c615fcb3b | ||
|
|
3ca900af6c | ||
|
|
1481626b1b | ||
|
|
a0ba5aeca4 | ||
|
|
f48a922b4c | ||
|
|
2418273191 | ||
|
|
09a10d7838 | ||
|
|
40ed1dd612 | ||
|
|
2aa38f1117 | ||
|
|
5d81ad9830 | ||
|
|
dffb45e233 | ||
|
|
5606c7b29a | ||
|
|
9d5b4f63d3 | ||
|
|
2c353023ab | ||
|
|
6992cdbaa4 | ||
|
|
9576079bfa | ||
|
|
1e08ccaae3 | ||
|
|
80e07eb53a | ||
|
|
54b7a27e65 | ||
|
|
5dc6100e8e | ||
|
|
a5c1a7122d | ||
|
|
61442462c7 | ||
|
|
917c31bef4 | ||
|
|
989439587f | ||
|
|
e74130733e | ||
|
|
2cce9a4f4c | ||
|
|
b721dfeb71 | ||
|
|
645b9b2c37 | ||
|
|
b419ebfe5d | ||
|
|
96d40a2946 | ||
|
|
e9b40573c7 | ||
|
|
ad316dfeac | ||
|
|
62cbaf3c04 | ||
|
|
cdb7eb3da9 | ||
|
|
671e0bbf71 | ||
|
|
50704b7b6a | ||
|
|
c198d1f593 | ||
|
|
c91cd2eecf | ||
|
|
31e47f4a04 | ||
|
|
470a9c4afa | ||
|
|
98692be3ba | ||
|
|
5c5077d7ec | ||
|
|
c13cae9ab3 | ||
|
|
74de687b90 | ||
|
|
ed3aca7d0c | ||
|
|
c9467f31c8 | ||
|
|
ad08632809 | ||
|
|
bea47b2640 | ||
|
|
242078ec15 | ||
|
|
5ca140df46 | ||
|
|
1259212cb2 | ||
|
|
299c43e10d | ||
|
|
969450cad3 | ||
|
|
3da330b637 | ||
|
|
86bd64e887 | ||
|
|
e02f939cb8 | ||
|
|
4bde6e2d1f | ||
|
|
ee69783a66 | ||
|
|
cddb0e94fa | ||
|
|
b1277ff2ac | ||
|
|
262c1fe1e2 | ||
|
|
2d2603ff27 | ||
|
|
d349e43523 | ||
|
|
ebf2d7d5c6 | ||
|
|
411c618be1 | ||
|
|
6151dcb8ff | ||
|
|
3564abfa29 | ||
|
|
955becdb46 | ||
|
|
daba3a3f28 | ||
|
|
33b8d1a1f5 | ||
|
|
5e35c23883 | ||
|
|
601c369f2d | ||
|
|
d48925f9ba |
51
.github/workflows/ci.yml
vendored
@@ -24,45 +24,16 @@ jobs:
|
|||||||
DOCKER_REGISTRY_URL: tip-tip-wlan-cloud-ucentral.jfrog.io
|
DOCKER_REGISTRY_URL: tip-tip-wlan-cloud-ucentral.jfrog.io
|
||||||
DOCKER_REGISTRY_USERNAME: ucentral
|
DOCKER_REGISTRY_USERNAME: ucentral
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout actions repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
- name: Build Docker image
|
|
||||||
run: docker build -t owgw-ui:${{ github.sha }} .
|
|
||||||
|
|
||||||
- name: Tag Docker image
|
|
||||||
run: |
|
|
||||||
TAGS="${{ github.sha }}"
|
|
||||||
|
|
||||||
if [[ ${GITHUB_REF} == "refs/heads/"* ]]
|
|
||||||
then
|
|
||||||
CURRENT_TAG=$(echo ${GITHUB_REF#refs/heads/} | tr '/' '-')
|
|
||||||
TAGS="$TAGS $CURRENT_TAG"
|
|
||||||
else
|
|
||||||
if [[ ${GITHUB_REF} == "refs/tags/"* ]]
|
|
||||||
then
|
|
||||||
CURRENT_TAG=$(echo ${GITHUB_REF#refs/tags/} | tr '/' '-')
|
|
||||||
TAGS="$TAGS $CURRENT_TAG"
|
|
||||||
else # PR build
|
|
||||||
CURRENT_TAG=$(echo ${GITHUB_HEAD_REF#refs/heads/} | tr '/' '-')
|
|
||||||
TAGS="$TAGS $CURRENT_TAG"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Result tags: $TAGS"
|
|
||||||
|
|
||||||
for tag in $TAGS; do
|
|
||||||
docker tag owgw-ui:${{ github.sha }} ${{ env.DOCKER_REGISTRY_URL }}/owgw-ui:$tag
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Log into Docker registry
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/pull/') || github.ref == 'refs/heads/main'
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKER_REGISTRY_URL }}
|
repository: Telecominfraproject/.github
|
||||||
username: ${{ env.DOCKER_REGISTRY_USERNAME }}
|
path: github
|
||||||
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Push Docker images
|
- name: Build and push Docker image
|
||||||
if: startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/pull/') || github.ref == 'refs/heads/main'
|
uses: ./github/composite-actions/docker-image-build
|
||||||
run: |
|
with:
|
||||||
docker images | grep ${{ env.DOCKER_REGISTRY_URL }}/owgw-ui | awk -F ' ' '{print $1":"$2}' | xargs -I {} docker push {}
|
image_name: owgw-ui
|
||||||
|
registry: tip-tip-wlan-cloud-ucentral.jfrog.io
|
||||||
|
registry_user: ucentral
|
||||||
|
registry_password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||||||
|
|||||||
24
.github/workflows/enforce-jira-issue-key.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Ensure Jira issue is linked
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited, reopened, synchronize]
|
||||||
|
branches:
|
||||||
|
- 'release/*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_for_issue_key:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout actions repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: Telecominfraproject/.github
|
||||||
|
path: github
|
||||||
|
|
||||||
|
- name: Run JIRA check
|
||||||
|
uses: ./github/composite-actions/enforce-jira-issue-key
|
||||||
|
with:
|
||||||
|
jira_base_url: ${{ secrets.TIP_JIRA_URL }}
|
||||||
|
jira_user_email: ${{ secrets.TIP_JIRA_USER_EMAIL }}
|
||||||
|
jira_api_token: ${{ secrets.TIP_JIRA_API_TOKEN }}
|
||||||
@@ -20,7 +20,7 @@ Currently this chart is not assembled in charts archives, so [helm-git](https://
|
|||||||
To install the chart with the release name `my-release`:
|
To install the chart with the release name `my-release`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ helm install --name my-release git+https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui@helm?ref=main
|
$ helm install --name my-release git+https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui@helm/owgwui-0.1.0.tgz?ref=main
|
||||||
```
|
```
|
||||||
|
|
||||||
The command deploys the Web UI on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
|
The command deploys the Web UI on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
|
||||||
|
|||||||
@@ -30,3 +30,13 @@ Create chart name and version as used by the chart label.
|
|||||||
{{- define "owgwui.chart" -}}
|
{{- define "owgwui.chart" -}}
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
{{- define "owgwui.ingress.apiVersion" -}}
|
||||||
|
{{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1" -}}
|
||||||
|
{{- print "networking.k8s.io/v1" -}}
|
||||||
|
{{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" -}}
|
||||||
|
{{- print "networking.k8s.io/v1beta1" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- print "extensions/v1beta1" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ metadata:
|
|||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ .Values.replicaCount }}
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app.kubernetes.io/name: {{ include "owgwui.name" . }}
|
app.kubernetes.io/name: {{ include "owgwui.name" . }}
|
||||||
@@ -26,6 +27,12 @@ spec:
|
|||||||
{{- with .Values.services.owgwui.labels }}
|
{{- with .Values.services.owgwui.labels }}
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- if .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
|
|
||||||
containers:
|
containers:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{{- range $ingress, $ingressValue := .Values.ingresses }}
|
{{- range $ingress, $ingressValue := .Values.ingresses }}
|
||||||
{{- if $ingressValue.enabled }}
|
{{- if $ingressValue.enabled }}
|
||||||
---
|
---
|
||||||
apiVersion: extensions/v1beta1
|
apiVersion: {{ include "owgwui.ingress.apiVersion" $root }}
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "owgwui.fullname" $root }}-{{ $ingress }}
|
name: {{ include "owgwui.fullname" $root }}-{{ $ingress }}
|
||||||
@@ -36,11 +36,25 @@ spec:
|
|||||||
paths:
|
paths:
|
||||||
{{- range $ingressValue.paths }}
|
{{- range $ingressValue.paths }}
|
||||||
- path: {{ .path }}
|
- path: {{ .path }}
|
||||||
|
{{- if $root.Capabilities.APIVersions.Has "networking.k8s.io/v1" }}
|
||||||
|
pathType: {{ .pathType | default "ImplementationSpecific" }}
|
||||||
|
{{- end }}
|
||||||
backend:
|
backend:
|
||||||
|
{{- if $root.Capabilities.APIVersions.Has "networking.k8s.io/v1" }}
|
||||||
|
service:
|
||||||
|
name: {{ include "owgwui.fullname" $root }}-{{ .serviceName }}
|
||||||
|
port:
|
||||||
|
{{- if kindIs "string" .servicePort }}
|
||||||
|
name: {{ .servicePort }}
|
||||||
|
{{- else }}
|
||||||
|
number: {{ .servicePort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else }}
|
||||||
serviceName: {{ include "owgwui.fullname" $root }}-{{ .serviceName }}
|
serviceName: {{ include "owgwui.fullname" $root }}-{{ .serviceName }}
|
||||||
servicePort: {{ .servicePort }}
|
servicePort: {{ .servicePort }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# System
|
# System
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
|
revisionHistoryLimit: 2
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
fullnameOverride: ""
|
fullnameOverride: ""
|
||||||
@@ -7,12 +8,12 @@ fullnameOverride: ""
|
|||||||
images:
|
images:
|
||||||
owgwui:
|
owgwui:
|
||||||
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
||||||
tag: main
|
tag: v2.5.2
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
|
|
||||||
services:
|
services:
|
||||||
owgwui:
|
owgwui:
|
||||||
type: ClusterIP
|
type: NodePort
|
||||||
ports:
|
ports:
|
||||||
http:
|
http:
|
||||||
servicePort: 80
|
servicePort: 80
|
||||||
@@ -48,6 +49,7 @@ ingresses:
|
|||||||
- chart-example.local
|
- chart-example.local
|
||||||
paths:
|
paths:
|
||||||
- path: /
|
- path: /
|
||||||
|
pathType: ImplementationSpecific
|
||||||
serviceName: owgwui
|
serviceName: owgwui
|
||||||
servicePort: http
|
servicePort: http
|
||||||
|
|
||||||
@@ -69,6 +71,8 @@ tolerations: []
|
|||||||
|
|
||||||
affinity: {}
|
affinity: {}
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
# Application
|
# Application
|
||||||
public_env_variables:
|
public_env_variables:
|
||||||
DEFAULT_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001
|
DEFAULT_UCENTRALSEC_URL: https://ucentral.dpaas.arilia.com:16001
|
||||||
|
|||||||
10488
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.4.3",
|
"version": "2.5.44",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coreui/coreui": "^3.4.0",
|
"@coreui/coreui": "^3.4.0",
|
||||||
"@coreui/icons": "^2.0.1",
|
"@coreui/icons": "^2.0.1",
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-apexcharts": "^1.3.9",
|
"react-apexcharts": "^1.3.9",
|
||||||
|
"react-csv": "^2.2.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-flow-renderer": "^9.6.6",
|
"react-flow-renderer": "^9.6.6",
|
||||||
"react-i18next": "^11.11.0",
|
"react-i18next": "^11.11.0",
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
"react-tooltip": "^4.2.21",
|
"react-tooltip": "^4.2.21",
|
||||||
"react-widgets": "^5.1.1",
|
"react-widgets": "^5.1.1",
|
||||||
"sass": "^1.35.1",
|
"sass": "^1.35.1",
|
||||||
"ucentral-libs": "^1.0.37",
|
"ucentral-libs": "^1.0.60",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -82,7 +83,6 @@
|
|||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"mini-css-extract-plugin": "^1.6.1",
|
"mini-css-extract-plugin": "^1.6.1",
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"prettier": "^2.3.2",
|
"prettier": "^2.3.2",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "LEDs Blinken",
|
"blink": "LEDs Blinken",
|
||||||
"device_leds": "LEDs",
|
"device_leds": "LEDs",
|
||||||
"execute_now": "Möchten Sie dieses Muster jetzt einstellen?",
|
"execute_now": "Möchten Sie dieses Muster jetzt einstellen?",
|
||||||
|
"explanation": "Welches Muster möchten Sie auf diesem Gerät für 30 Sekunden einstellen?",
|
||||||
"pattern": "Wählen Sie das Muster, das Sie verwenden möchten:",
|
"pattern": "Wählen Sie das Muster, das Sie verwenden möchten:",
|
||||||
"set_leds": "LEDs einstellen",
|
"set_leds": "LEDs einstellen",
|
||||||
"when_blink_leds": "Wann möchten Sie die LEDs blinken lassen?"
|
"when_blink_leds": "Wann möchten Sie die LEDs blinken lassen?"
|
||||||
@@ -37,13 +38,16 @@
|
|||||||
"add_note": "Notiz hinzufügen",
|
"add_note": "Notiz hinzufügen",
|
||||||
"add_note_explanation": "Schreiben Sie unten Ihre neue Notiz und klicken Sie auf die Schaltfläche \"+\", wo Sie fertig sind",
|
"add_note_explanation": "Schreiben Sie unten Ihre neue Notiz und klicken Sie auf die Schaltfläche \"+\", wo Sie fertig sind",
|
||||||
"adding_ellipsis": "Hinzufügen ...",
|
"adding_ellipsis": "Hinzufügen ...",
|
||||||
|
"all": "Alles",
|
||||||
"are_you_sure": "Bist du sicher?",
|
"are_you_sure": "Bist du sicher?",
|
||||||
"back_to_login": "Zurück zur Anmeldung",
|
"back_to_login": "Zurück zur Anmeldung",
|
||||||
"back_to_start": "Zurück zum Start",
|
"back_to_start": "Zurück zum Start",
|
||||||
|
"blacklist": "Schwarze Liste",
|
||||||
"by": "Durch",
|
"by": "Durch",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"certificate": "Zertifikat",
|
"certificate": "Zertifikat",
|
||||||
"certificates": "Zertifikate",
|
"certificates": "Zertifikate",
|
||||||
|
"claim": "Anspruch",
|
||||||
"clear": "Löschen",
|
"clear": "Löschen",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
@@ -58,16 +62,19 @@
|
|||||||
"confirm_stop_editing": "Möchten Sie die Bearbeitung wirklich beenden? Dadurch werden alle nicht gespeicherten Änderungen, die Sie vorgenommen haben, verworfen.",
|
"confirm_stop_editing": "Möchten Sie die Bearbeitung wirklich beenden? Dadurch werden alle nicht gespeicherten Änderungen, die Sie vorgenommen haben, verworfen.",
|
||||||
"connected": "Verbindung wurde hergestellt",
|
"connected": "Verbindung wurde hergestellt",
|
||||||
"copied": "kopiert!",
|
"copied": "kopiert!",
|
||||||
|
"copied_to_clipboard": "In die Zwischenablage kopiert!",
|
||||||
"copy_to_clipboard": "In die Zwischenablage kopieren",
|
"copy_to_clipboard": "In die Zwischenablage kopieren",
|
||||||
"create": "Erstellen",
|
"create": "Erstellen",
|
||||||
"created": "Erstellt",
|
"created": "Erstellt",
|
||||||
"created_by": "Erstellt von",
|
"created_by": "Erstellt von",
|
||||||
|
"creator": "Schöpfer",
|
||||||
"current": "Aktuell",
|
"current": "Aktuell",
|
||||||
"custom_date": "Benutzerdefiniertes Datum",
|
"custom_date": "Benutzerdefiniertes Datum",
|
||||||
"dashboard": "Instrumententafel",
|
"dashboard": "Instrumententafel",
|
||||||
"date": "Datum",
|
"date": "Datum",
|
||||||
"day": "tag",
|
"day": "tag",
|
||||||
"days": "tage",
|
"days": "tage",
|
||||||
|
"default_map": "Standardkarte",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"delete_device": "Gerät löschen",
|
"delete_device": "Gerät löschen",
|
||||||
"details": "Einzelheiten",
|
"details": "Einzelheiten",
|
||||||
@@ -85,6 +92,7 @@
|
|||||||
"dismiss": "entlassen",
|
"dismiss": "entlassen",
|
||||||
"do_now": "Sofort",
|
"do_now": "Sofort",
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
|
"duplicate": "Duplikat",
|
||||||
"duration": "Dauer",
|
"duration": "Dauer",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"edit_user": "Bearbeiten",
|
"edit_user": "Bearbeiten",
|
||||||
@@ -94,6 +102,7 @@
|
|||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"error_adding_note": "Fehler beim Hinzufügen einer Notiz",
|
"error_adding_note": "Fehler beim Hinzufügen einer Notiz",
|
||||||
"error_code": "Fehlercode",
|
"error_code": "Fehlercode",
|
||||||
|
"errors": "Fehler",
|
||||||
"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?",
|
"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?",
|
||||||
"executed": "Ausgeführt",
|
"executed": "Ausgeführt",
|
||||||
"exit": "Ausgang",
|
"exit": "Ausgang",
|
||||||
@@ -110,6 +119,7 @@
|
|||||||
"hours": "std",
|
"hours": "std",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"invalid_credentials": "Ungültiger Benutzername und / oder Passwort",
|
"invalid_credentials": "Ungültiger Benutzername und / oder Passwort",
|
||||||
|
"invalid_date_explanation": "Ungültiges Datum, bitte verwenden Sie den Kalender, auf den Sie über die Schaltfläche rechts zugreifen können",
|
||||||
"invalid_file": "Die ausgewählte Datei war ungültig, bitte lesen Sie die Anweisungen und passen Sie Ihre Datei entsprechend an",
|
"invalid_file": "Die ausgewählte Datei war ungültig, bitte lesen Sie die Anweisungen und passen Sie Ihre Datei entsprechend an",
|
||||||
"invalid_password": "Dieses Passwort entspricht nicht den grundlegenden Passwortregeln. Bitte besuchen Sie unsere Seite Passwortrichtlinien, um mehr zu erfahren",
|
"invalid_password": "Dieses Passwort entspricht nicht den grundlegenden Passwortregeln. Bitte besuchen Sie unsere Seite Passwortrichtlinien, um mehr zu erfahren",
|
||||||
"invalid_pem": "Ihre PEM-Datei ist ungültig. Es sollte mit '-----BEGIN CERTIFICATE-----' ODER '-----BEGIN PRIVATE KEY-----' beginnen und mit '-----END CERTIFICATE--- enden. --' ODER '-----END PRIVATSCHLÜSSEL-----'",
|
"invalid_pem": "Ihre PEM-Datei ist ungültig. Es sollte mit '-----BEGIN CERTIFICATE-----' ODER '-----BEGIN PRIVATE KEY-----' beginnen und mit '-----END CERTIFICATE--- enden. --' ODER '-----END PRIVATSCHLÜSSEL-----'",
|
||||||
@@ -142,17 +152,20 @@
|
|||||||
"no_items": "Keine Gegenstände",
|
"no_items": "Keine Gegenstände",
|
||||||
"none": "Keiner",
|
"none": "Keiner",
|
||||||
"not_connected": "Nicht verbunden",
|
"not_connected": "Nicht verbunden",
|
||||||
"of_connected": "% der Geräte",
|
"of_connected": "% der verbundenen Geräte",
|
||||||
"off": "Aus",
|
"off": "Aus",
|
||||||
"on": "An",
|
"on": "An",
|
||||||
"optional": "Wahlweise",
|
"optional": "Wahlweise",
|
||||||
"overall_health": "Allgemeine Gesundheit",
|
"overall_health": "Allgemeine Gesundheit",
|
||||||
"password_policy": "Kennwortrichtlinie",
|
"password_policy": "Kennwortrichtlinie",
|
||||||
|
"preferences": "Einstellungen",
|
||||||
"preview": "Vorschau",
|
"preview": "Vorschau",
|
||||||
"program": "Programm",
|
"program": "Programm",
|
||||||
|
"reason": "Grund",
|
||||||
"recorded": "Verzeichnet",
|
"recorded": "Verzeichnet",
|
||||||
"refresh": "Aktualisierung",
|
"refresh": "Aktualisierung",
|
||||||
"refresh_device": "Gerät aktualisieren",
|
"refresh_device": "Gerät aktualisieren",
|
||||||
|
"remove_claim": "Anspruch entfernen",
|
||||||
"required": "Erforderlich",
|
"required": "Erforderlich",
|
||||||
"result": "Ergebnis",
|
"result": "Ergebnis",
|
||||||
"save": "Sparen",
|
"save": "Sparen",
|
||||||
@@ -170,12 +183,14 @@
|
|||||||
"show_all": "Zeige alles",
|
"show_all": "Zeige alles",
|
||||||
"socket_connection_closed": "Verbindung geschlossen!",
|
"socket_connection_closed": "Verbindung geschlossen!",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"status": "Status",
|
||||||
"stop_editing": "Stoppen Sie die Bearbeitung",
|
"stop_editing": "Stoppen Sie die Bearbeitung",
|
||||||
"submit": "Absenden",
|
"submit": "Absenden",
|
||||||
"submitted": "Eingereicht",
|
"submitted": "Eingereicht",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"table": "Tabelle",
|
"table": "Tabelle",
|
||||||
|
"time_per_device": "Gerät/Sekunde",
|
||||||
"timestamp": "Zeit",
|
"timestamp": "Zeit",
|
||||||
"to": "zu",
|
"to": "zu",
|
||||||
"type": "Art",
|
"type": "Art",
|
||||||
@@ -190,6 +205,8 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Anbieter",
|
"vendors": "Anbieter",
|
||||||
"view_more": "Mehr anzeigen",
|
"view_more": "Mehr anzeigen",
|
||||||
|
"visibility": "Sichtweite",
|
||||||
|
"waiting_for_update": "Warten auf Aktualisierung",
|
||||||
"yes": "Ja"
|
"yes": "Ja"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +227,8 @@
|
|||||||
"creation_success": "Konfiguration erfolgreich erstellt!",
|
"creation_success": "Konfiguration erfolgreich erstellt!",
|
||||||
"currently_associated": "Aktuell zugeordnete Konfiguration: {{config}}",
|
"currently_associated": "Aktuell zugeordnete Konfiguration: {{config}}",
|
||||||
"currently_selected_config": "Derzeit ausgewählte Konfiguration: {{config}}",
|
"currently_selected_config": "Derzeit ausgewählte Konfiguration: {{config}}",
|
||||||
|
"default_configs": "Standardkonfigurationen",
|
||||||
|
"default_configurations": "Standardkonfigurationen",
|
||||||
"delete_config": "Konfiguration löschen",
|
"delete_config": "Konfiguration löschen",
|
||||||
"details": "Gerätedetails",
|
"details": "Gerätedetails",
|
||||||
"device_password": "Passwort",
|
"device_password": "Passwort",
|
||||||
@@ -218,6 +237,7 @@
|
|||||||
"devices_affected": "Von dieser Konfiguration betroffene Geräte:",
|
"devices_affected": "Von dieser Konfiguration betroffene Geräte:",
|
||||||
"edit_configuration": "Konfiguration bearbeiten",
|
"edit_configuration": "Konfiguration bearbeiten",
|
||||||
"error_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
"error_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
||||||
|
"error_delete_blacklist": "Fehler beim Löschen aus der schwarzen Liste: {{error}}",
|
||||||
"error_fetching_config": "Fehler beim Abrufen der Konfiguration",
|
"error_fetching_config": "Fehler beim Abrufen der Konfiguration",
|
||||||
"error_trying_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
"error_trying_delete": "Fehler beim Versuch zu löschen: {{error}}",
|
||||||
"error_update": "Fehler: {{error}}",
|
"error_update": "Fehler: {{error}}",
|
||||||
@@ -261,6 +281,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "Zugangs-PIN",
|
"access_pin": "Zugangs-PIN",
|
||||||
"add_contact": "Kontakt hinzufügen",
|
"add_contact": "Kontakt hinzufügen",
|
||||||
|
"contact": "Kontakt",
|
||||||
"create_contact": "Kontakt erstellen",
|
"create_contact": "Kontakt erstellen",
|
||||||
"currently_selected_contact": "Aktuell ausgewählter Kontakt: {{contact}}",
|
"currently_selected_contact": "Aktuell ausgewählter Kontakt: {{contact}}",
|
||||||
"delete": "Kontakt löschen?",
|
"delete": "Kontakt löschen?",
|
||||||
@@ -300,12 +321,23 @@
|
|||||||
"healthchecks_title": "Healthchecks löschen"
|
"healthchecks_title": "Healthchecks löschen"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Gerät zur Blacklist hinzufügen",
|
||||||
|
"all_devices": "Alle Geräte",
|
||||||
|
"blacklisted_on": "Datum",
|
||||||
|
"capabilities": "Fähigkeiten",
|
||||||
"certificate_explanation": "Zertifikate der angeschlossenen Geräte",
|
"certificate_explanation": "Zertifikate der angeschlossenen Geräte",
|
||||||
|
"edit_blacklist": "Gerät auf der schwarzen Liste bearbeiten",
|
||||||
|
"error_adding_blacklist": "Fehler beim Hinzufügen des Geräts zur Blacklist: {{error}}",
|
||||||
|
"error_edit_blacklist": "Fehler beim Bearbeiten der schwarzen Liste: {{error}}",
|
||||||
"error_fetching_device": "Fehler beim Abrufen der Geräteinformationen: {{error}}",
|
"error_fetching_device": "Fehler beim Abrufen der Geräteinformationen: {{error}}",
|
||||||
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}",
|
"error_fetching_devices": "Fehler beim Abrufen von Geräten: {{error}}",
|
||||||
"health_explanation": "Zustand der angeschlossenen Geräte",
|
"health_explanation": "Zustand der verbundenen Geräte ((Geräte = 100 % * 100 + Geräte > 90 % * 95 + Geräte > 60 % * 75 + Geräte < 60 % * 35) / Verbundene Geräte)",
|
||||||
"memory_explanation": "Von angeschlossenen Geräten belegter Speicher",
|
"memory_explanation": "Anzahl verbundener Geräte mit entsprechendem belegtem Speicher %",
|
||||||
"uptimes_explanation": "Zeit, zu der verbundene Geräte aktiv und verbunden waren"
|
"remove_from_blacklist": "Von der schwarzen Liste entfernen",
|
||||||
|
"success_added_blacklist": "Gerät erfolgreich zur Blacklist hinzugefügt!",
|
||||||
|
"success_edit_blacklist": "Blacklist erfolgreich bearbeitet!",
|
||||||
|
"success_removed_blacklist": "Gerät erfolgreich von Blacklist entfernt!",
|
||||||
|
"uptimes_explanation": "Anzahl der verbundenen Geräte basierend auf ihrer Betriebszeit"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Protokoll",
|
"log": "Protokoll",
|
||||||
@@ -320,23 +352,32 @@
|
|||||||
"add_success": "Entität erfolgreich erstellt!",
|
"add_success": "Entität erfolgreich erstellt!",
|
||||||
"assigned_inventory": "Zugewiesenes Inventar",
|
"assigned_inventory": "Zugewiesenes Inventar",
|
||||||
"cannot_delete": "Entitäten mit untergeordneten Elementen können nicht gelöscht werden. Löschen Sie die untergeordneten Elemente dieser Entität, um sie löschen zu können.",
|
"cannot_delete": "Entitäten mit untergeordneten Elementen können nicht gelöscht werden. Löschen Sie die untergeordneten Elemente dieser Entität, um sie löschen zu können.",
|
||||||
|
"confirm_map_delete": "Möchten Sie die Karte {{name}}wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden",
|
||||||
"currently_selected_entity": "Derzeit ausgewähltes Unternehmen: {{config}}",
|
"currently_selected_entity": "Derzeit ausgewähltes Unternehmen: {{config}}",
|
||||||
"currently_selected_venue": "Aktuell ausgewählter Veranstaltungsort: {{config}}",
|
"currently_selected_venue": "Aktuell ausgewählter Veranstaltungsort: {{config}}",
|
||||||
"delete_success": "Entität erfolgreich gelöscht",
|
"delete_success": "Entität erfolgreich gelöscht",
|
||||||
"delete_warning": "Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden",
|
"delete_warning": "Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden",
|
||||||
|
"duplicate_from_node": "Mit einem bestimmten Root-Knoten duplizieren",
|
||||||
|
"duplicate_map": "Karte duplizieren",
|
||||||
|
"duplicate_with_node": "Dupliziere {{mapName}} mit {{rootName}} als Root-Knoten",
|
||||||
"edit_failure": "Aktualisierung fehlgeschlagen : {{error}}",
|
"edit_failure": "Aktualisierung fehlgeschlagen : {{error}}",
|
||||||
"enter_here": "Geben Sie hier die IP(s) ein, die Sie hinzufügen möchten",
|
"enter_here": "Geben Sie hier die IP(s) ein, die Sie hinzufügen möchten",
|
||||||
"entire_tree": "Seitenverzeichnis",
|
"entire_tree": "Netzwerkkarte",
|
||||||
"entities": "Entitäten",
|
"entities": "Entitäten",
|
||||||
"entity": "Entität",
|
"entity": "Entität",
|
||||||
|
"error_deleting_map": "Fehler beim Löschen der Karte: {{error}}",
|
||||||
"error_fetch_entity": "Fehler beim Abrufen von Entitätsinformationen",
|
"error_fetch_entity": "Fehler beim Abrufen von Entitätsinformationen",
|
||||||
"error_fetching": "Fehler beim Abrufen von Entitäten",
|
"error_fetching": "Fehler beim Abrufen von Entitäten",
|
||||||
"error_fetching_map": "Fehler beim Abrufen der Karte: {{error}}",
|
"error_fetching_map": "Fehler beim Abrufen der Karte: {{error}}",
|
||||||
|
"error_fetching_tree": "Fehler beim Abrufen des Baums: {{error}}",
|
||||||
"error_saving": "Fehler beim Speichern der Entität",
|
"error_saving": "Fehler beim Speichern der Entität",
|
||||||
|
"error_saving_map": "Fehler beim Speichern der Karte: {{error}}",
|
||||||
"higher_priority": "Stellen Sie eine höhere Priorität ein",
|
"higher_priority": "Stellen Sie eine höhere Priorität ein",
|
||||||
"ip_detection": "IP-Erkennung",
|
"ip_detection": "IP-Erkennung",
|
||||||
"ip_formats": "Sie können IPv4- oder IPv6-Adressen in den folgenden Formaten hinzufügen:",
|
"ip_formats": "Sie können IPv4- oder IPv6-Adressen in den folgenden Formaten hinzufügen:",
|
||||||
"lower_priority": "Niedrigere Priorität setzen",
|
"lower_priority": "Niedrigere Priorität setzen",
|
||||||
|
"map": "Karte",
|
||||||
|
"map_delete_success": "Karte erfolgreich gelöscht!",
|
||||||
"need_select_entity": "sSie müssen eine Entität aus der folgenden Tabelle auswählen",
|
"need_select_entity": "sSie müssen eine Entität aus der folgenden Tabelle auswählen",
|
||||||
"no_ips": "Keine IPs ausgewählt",
|
"no_ips": "Keine IPs ausgewählt",
|
||||||
"not_assigned": "Nicht zugeordnet",
|
"not_assigned": "Nicht zugeordnet",
|
||||||
@@ -344,6 +385,7 @@
|
|||||||
"select_entity": "Wählen Sie diese Entität aus",
|
"select_entity": "Wählen Sie diese Entität aus",
|
||||||
"selected_entity": "Ausgewählte Einheit",
|
"selected_entity": "Ausgewählte Einheit",
|
||||||
"selected_map": "Ausgewählte Karte",
|
"selected_map": "Ausgewählte Karte",
|
||||||
|
"tree_saved": "Karte erfolgreich gespeichert!",
|
||||||
"update_failure_error": "Fehler beim Versuch, die Entität zu aktualisieren: {{error}}",
|
"update_failure_error": "Fehler beim Versuch, die Entität zu aktualisieren: {{error}}",
|
||||||
"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)",
|
"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)",
|
||||||
"venues": "Veranstaltungsorte"
|
"venues": "Veranstaltungsorte"
|
||||||
@@ -541,6 +583,9 @@
|
|||||||
"verification_code": "Geben Sie hier Ihre Bestätigung ein",
|
"verification_code": "Geben Sie hier Ihre Bestätigung ein",
|
||||||
"wrong_code": "Der eingegebene Bestätigungscode ist ungültig."
|
"wrong_code": "Der eingegebene Bestätigungscode ist ungültig."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Bereitstellung"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "Wann möchten Sie dieses Gerät neu starten?",
|
"directions": "Wann möchten Sie dieses Gerät neu starten?",
|
||||||
"now": "Möchten Sie dieses Gerät jetzt neu starten?",
|
"now": "Möchten Sie dieses Gerät jetzt neu starten?",
|
||||||
@@ -551,7 +596,7 @@
|
|||||||
"channel": "Kanal",
|
"channel": "Kanal",
|
||||||
"directions": "Starten Sie einen WiFi-Scan dieses Geräts, der ungefähr 25 Sekunden dauern sollte.",
|
"directions": "Starten Sie einen WiFi-Scan dieses Geräts, der ungefähr 25 Sekunden dauern sollte.",
|
||||||
"re_scan": "Erneut scannen",
|
"re_scan": "Erneut scannen",
|
||||||
"result_directions": "Bitte klicken Sie auf die Schaltfläche '$t(scan.re_scan)', wenn Sie einen Scan mit derselben Konfiguration wie beim letzten Scan durchführen möchten.",
|
"result_directions": "Sie können oben rechts auf die Schaltfläche „Scannen“ klicken, um $t(scan.re_scan)",
|
||||||
"results": "Ergebnisse des WiFi-Scans",
|
"results": "Ergebnisse des WiFi-Scans",
|
||||||
"scan": "Scan",
|
"scan": "Scan",
|
||||||
"scanning": "Scannen... ",
|
"scanning": "Scannen... ",
|
||||||
@@ -584,7 +629,7 @@
|
|||||||
"mac_prefix": "MAC-Präfix",
|
"mac_prefix": "MAC-Präfix",
|
||||||
"max_associations": "max. Verbände",
|
"max_associations": "max. Verbände",
|
||||||
"max_clients": "Max. Kunden",
|
"max_clients": "Max. Kunden",
|
||||||
"messages_transmitted": "Gesendete Nachrichten",
|
"messages_transmitted": "Nachricht TX",
|
||||||
"min_associations": "Mindest. Verbände",
|
"min_associations": "Mindest. Verbände",
|
||||||
"min_clients": "Mindest. Kunden",
|
"min_clients": "Mindest. Kunden",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
@@ -592,7 +637,7 @@
|
|||||||
"prefix_length": "Erforderlich, muss eine Länge von 6 Zeichen haben",
|
"prefix_length": "Erforderlich, muss eine Länge von 6 Zeichen haben",
|
||||||
"previous_runs": "Vorherige Läufe",
|
"previous_runs": "Vorherige Läufe",
|
||||||
"received": "empfangen",
|
"received": "empfangen",
|
||||||
"received_messages": "Erhaltene Nachrichten",
|
"received_messages": "Nachricht RX",
|
||||||
"reconnect_interval": "Wiederverbindungsintervall",
|
"reconnect_interval": "Wiederverbindungsintervall",
|
||||||
"resume": "Fortsetzen",
|
"resume": "Fortsetzen",
|
||||||
"resume_success": "Lauf wieder aufgenommen!",
|
"resume_success": "Lauf wieder aufgenommen!",
|
||||||
@@ -615,11 +660,14 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"data": "Daten (KB)",
|
"data": "Daten (KB)",
|
||||||
|
"data_mb": "Daten (MB)",
|
||||||
"latest_statistics": "Neueste Statistiken",
|
"latest_statistics": "Neueste Statistiken",
|
||||||
"lifetime_stats": "Lifetime-Statistik",
|
"lifetime_stats": "Lifetime-Statistik",
|
||||||
|
"memory": "Erinnerung",
|
||||||
"no_interfaces": "Keine Statistiken zur Schnittstellenlebensdauer verfügbar",
|
"no_interfaces": "Keine Statistiken zur Schnittstellenlebensdauer verfügbar",
|
||||||
"show_latest": "Letzte Statistik",
|
"show_latest": "Letzte Statistik",
|
||||||
"title": "Statistiken"
|
"title": "Statistiken",
|
||||||
|
"used": "Verwendeter Speicher %"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connection_status": "Status",
|
"connection_status": "Status",
|
||||||
@@ -631,9 +679,27 @@
|
|||||||
"percentage_free": "{{percentage}}% von {{total}} kostenlos",
|
"percentage_free": "{{percentage}}% von {{total}} kostenlos",
|
||||||
"percentage_used": "{{percentage}}% von {{total}} verwendet",
|
"percentage_used": "{{percentage}}% von {{total}} verwendet",
|
||||||
"title": "#{{serialNumber}} Status",
|
"title": "#{{serialNumber}} Status",
|
||||||
|
"total_memory": "Gesamtspeicher",
|
||||||
"uptime": "Betriebszeit",
|
"uptime": "Betriebszeit",
|
||||||
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
|
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"add_device_subscriber_explanation": "Um andere Geräte zu reklamieren, kannst du unsere Suchleiste verwenden oder direkt aus der Tabelle reklamieren. Wenn ein Gerät bereits von einem Benutzer beansprucht wurde, müssen Sie zu dessen Details gehen und die Zuweisung aufheben, bevor Sie es beanspruchen.",
|
||||||
|
"create": "Abonnenten erstellen",
|
||||||
|
"devices_one": "{{count}} Gerät",
|
||||||
|
"devices_other": "{{count}} Geräte",
|
||||||
|
"edit": "Abonnent bearbeiten",
|
||||||
|
"error_create": "Fehler beim Erstellen des Abonnenten: {{error}}",
|
||||||
|
"error_delete": "Fehler beim Löschen des Abonnenten: {{error}}",
|
||||||
|
"error_fetching": "Fehler beim Abrufen von Abonnenten: {{error}}",
|
||||||
|
"error_fetching_single": "Fehler beim Abrufen des Abonnenten: {{error}}",
|
||||||
|
"error_update": "Fehler beim Aktualisieren des Abonnenten: {{error}}",
|
||||||
|
"is_already_claimed": "wird bereits beansprucht von",
|
||||||
|
"subscribers": "Abonnenten",
|
||||||
|
"success_create": "Abonnent erfolgreich erstellt!",
|
||||||
|
"success_delete": "Abonnent erfolgreich gelöscht!",
|
||||||
|
"success_update": "Abonnent erfolgreich aktualisiert!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Fehler beim Abrufen von Systeminformationen",
|
"error_fetching": "Fehler beim Abrufen von Systeminformationen",
|
||||||
"error_reloading": "Fehler beim Neuladen: {{error}}",
|
"error_reloading": "Fehler beim Neuladen: {{error}}",
|
||||||
@@ -723,6 +789,7 @@
|
|||||||
"send_code_again": "Code nochmal senden",
|
"send_code_again": "Code nochmal senden",
|
||||||
"show_hide_password": "Passwort anzeigen/verbergen",
|
"show_hide_password": "Passwort anzeigen/verbergen",
|
||||||
"successful_validation": "Telefonnummer bestätigt! Klicken Sie auf die Schaltfläche Speichern, um es mit Ihrem Profil zu verknüpfen",
|
"successful_validation": "Telefonnummer bestätigt! Klicken Sie auf die Schaltfläche Speichern, um es mit Ihrem Profil zu verknüpfen",
|
||||||
|
"table_title": "Admin-Benutzer",
|
||||||
"update_failure": "Fehler beim Aktualisieren: {{error}}",
|
"update_failure": "Fehler beim Aktualisieren: {{error}}",
|
||||||
"update_failure_title": "Update fehlgeschlagen",
|
"update_failure_title": "Update fehlgeschlagen",
|
||||||
"update_success": "Benutzer erfolgreich aktualisiert",
|
"update_success": "Benutzer erfolgreich aktualisiert",
|
||||||
@@ -738,7 +805,10 @@
|
|||||||
"associations": "Verbände",
|
"associations": "Verbände",
|
||||||
"mode": "Modus",
|
"mode": "Modus",
|
||||||
"network_diagram": "Netzwerkdiagramm",
|
"network_diagram": "Netzwerkdiagramm",
|
||||||
|
"override_dfs": "DFS überschreiben",
|
||||||
"radios": "Radios",
|
"radios": "Radios",
|
||||||
"title": "WLAN-Analyse"
|
"scan_warning": "Ihr 5G-Funkgerät befindet sich auf einem Radarkanal, Sie müssen „Override DFS“ aktivieren, um das Scannen aller 5G-Kanäle zu ermöglichen",
|
||||||
|
"title": "WLAN-Analyse",
|
||||||
|
"vendor": "Verkäufer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Blink",
|
"blink": "Blink",
|
||||||
"device_leds": "Device LEDs",
|
"device_leds": "Device LEDs",
|
||||||
"execute_now": "Would you like to set this pattern now?",
|
"execute_now": "Would you like to set this pattern now?",
|
||||||
|
"explanation": "What pattern would you like to set on this device for 30 seconds?",
|
||||||
"pattern": "LEDs pattern: ",
|
"pattern": "LEDs pattern: ",
|
||||||
"set_leds": "Set LEDs",
|
"set_leds": "Set LEDs",
|
||||||
"when_blink_leds": "When would you like to make the device LEDs blink?"
|
"when_blink_leds": "When would you like to make the device LEDs blink?"
|
||||||
@@ -37,13 +38,16 @@
|
|||||||
"add_note": "Add Note",
|
"add_note": "Add Note",
|
||||||
"add_note_explanation": "Write your new note below and click the '+' button where you are done",
|
"add_note_explanation": "Write your new note below and click the '+' button where you are done",
|
||||||
"adding_ellipsis": "Adding...",
|
"adding_ellipsis": "Adding...",
|
||||||
|
"all": "All",
|
||||||
"are_you_sure": "Are you sure?",
|
"are_you_sure": "Are you sure?",
|
||||||
"back_to_login": "Back to Login",
|
"back_to_login": "Back to Login",
|
||||||
"back_to_start": "Back to start",
|
"back_to_start": "Back to start",
|
||||||
|
"blacklist": "Blacklist",
|
||||||
"by": "By",
|
"by": "By",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"certificate": "Certificate",
|
"certificate": "Certificate",
|
||||||
"certificates": "Certificates",
|
"certificates": "Certificates",
|
||||||
|
"claim": "Claim",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
@@ -58,16 +62,19 @@
|
|||||||
"confirm_stop_editing": "Are you sure you want to stop editing? This will cancel any unsaved changes you have made.",
|
"confirm_stop_editing": "Are you sure you want to stop editing? This will cancel any unsaved changes you have made.",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
|
"copied_to_clipboard": "Copied to Clipboard!",
|
||||||
"copy_to_clipboard": "Copy to clipboard",
|
"copy_to_clipboard": "Copy to clipboard",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
"created_by": "Created By",
|
"created_by": "Created By",
|
||||||
|
"creator": "Creator",
|
||||||
"current": "Current ",
|
"current": "Current ",
|
||||||
"custom_date": "Custom Date",
|
"custom_date": "Custom Date",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
"day": "day",
|
"day": "day",
|
||||||
"days": "days",
|
"days": "days",
|
||||||
|
"default_map": "Default Map",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_device": "Delete Device",
|
"delete_device": "Delete Device",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
@@ -85,6 +92,7 @@
|
|||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"do_now": "Do Now!",
|
"do_now": "Do Now!",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"edit_user": "Edit",
|
"edit_user": "Edit",
|
||||||
@@ -94,6 +102,7 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_adding_note": "Error while adding note",
|
"error_adding_note": "Error while adding note",
|
||||||
"error_code": "Error Code",
|
"error_code": "Error Code",
|
||||||
|
"errors": "Errors",
|
||||||
"execute_now": "Would you like to execute this command now?",
|
"execute_now": "Would you like to execute this command now?",
|
||||||
"executed": "Executed",
|
"executed": "Executed",
|
||||||
"exit": "Exit",
|
"exit": "Exit",
|
||||||
@@ -110,6 +119,7 @@
|
|||||||
"hours": "hours",
|
"hours": "hours",
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"invalid_credentials": "Invalid username and/or password",
|
"invalid_credentials": "Invalid username and/or password",
|
||||||
|
"invalid_date_explanation": "Invalid Date, please use the calendar accessible with the button on the right ",
|
||||||
"invalid_file": "The chosen file was invalid, please read the instructions and adjust your file accordingly",
|
"invalid_file": "The chosen file was invalid, please read the instructions and adjust your file accordingly",
|
||||||
"invalid_password": "This password does not confirm to basic password rules. Please visit our Password Policy page to learn more",
|
"invalid_password": "This password does not confirm to basic password rules. Please visit our Password Policy page to learn more",
|
||||||
"invalid_pem": "Your .pem file is invalid. It should start with '-----BEGIN CERTIFICATE-----' OR '-----BEGIN PRIVATE KEY-----' and it should end with '-----END CERTIFICATE-----' OR '-----END PRIVATE KEY-----'",
|
"invalid_pem": "Your .pem file is invalid. It should start with '-----BEGIN CERTIFICATE-----' OR '-----BEGIN PRIVATE KEY-----' and it should end with '-----END CERTIFICATE-----' OR '-----END PRIVATE KEY-----'",
|
||||||
@@ -142,17 +152,20 @@
|
|||||||
"no_items": "No Items",
|
"no_items": "No Items",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"not_connected": "Not Connected",
|
"not_connected": "Not Connected",
|
||||||
"of_connected": "% of devices",
|
"of_connected": "% of connected devices",
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"on": "On",
|
"on": "On",
|
||||||
"optional": "Optional",
|
"optional": "Optional",
|
||||||
"overall_health": "Overall Health",
|
"overall_health": "Overall Health",
|
||||||
"password_policy": "Password Policy",
|
"password_policy": "Password Policy",
|
||||||
|
"preferences": "Preferences",
|
||||||
"preview": "Preview",
|
"preview": "Preview",
|
||||||
"program": "Program",
|
"program": "Program",
|
||||||
|
"reason": "Reason",
|
||||||
"recorded": "Recorded",
|
"recorded": "Recorded",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"refresh_device": "Refresh Device",
|
"refresh_device": "Refresh Device",
|
||||||
|
"remove_claim": "Remove Claim",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -170,12 +183,14 @@
|
|||||||
"show_all": "Show All",
|
"show_all": "Show All",
|
||||||
"socket_connection_closed": "Connection closed!",
|
"socket_connection_closed": "Connection closed!",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
|
"status": "Status",
|
||||||
"stop_editing": "Stop Editing",
|
"stop_editing": "Stop Editing",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"submitted": "Submitted",
|
"submitted": "Submitted",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
|
"time_per_device": "Devices/Second",
|
||||||
"timestamp": "Time",
|
"timestamp": "Time",
|
||||||
"to": "To",
|
"to": "To",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -190,6 +205,8 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendors",
|
"vendors": "Vendors",
|
||||||
"view_more": "View more",
|
"view_more": "View more",
|
||||||
|
"visibility": "Visibility",
|
||||||
|
"waiting_for_update": "Waiting for Update",
|
||||||
"yes": "Yes"
|
"yes": "Yes"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +227,8 @@
|
|||||||
"creation_success": "Configuration successfully created!",
|
"creation_success": "Configuration successfully created!",
|
||||||
"currently_associated": "Currently Associated Configuration: {{config}}",
|
"currently_associated": "Currently Associated Configuration: {{config}}",
|
||||||
"currently_selected_config": "Currently Selected Configuration: {{config}}",
|
"currently_selected_config": "Currently Selected Configuration: {{config}}",
|
||||||
|
"default_configs": "Default Configs",
|
||||||
|
"default_configurations": "Default Configurations",
|
||||||
"delete_config": "Delete Config",
|
"delete_config": "Delete Config",
|
||||||
"details": "Details",
|
"details": "Details",
|
||||||
"device_password": "Password",
|
"device_password": "Password",
|
||||||
@@ -218,6 +237,7 @@
|
|||||||
"devices_affected": "Devices affected by this configuration: ",
|
"devices_affected": "Devices affected by this configuration: ",
|
||||||
"edit_configuration": "Edit Configuration",
|
"edit_configuration": "Edit Configuration",
|
||||||
"error_delete": "Error while trying to delete: {{error}}",
|
"error_delete": "Error while trying to delete: {{error}}",
|
||||||
|
"error_delete_blacklist": "Error deleting from blacklist: {{error}}",
|
||||||
"error_fetching_config": "Error while fetching configuration",
|
"error_fetching_config": "Error while fetching configuration",
|
||||||
"error_trying_delete": "Error while trying to delete: {{error}}",
|
"error_trying_delete": "Error while trying to delete: {{error}}",
|
||||||
"error_update": "Error: {{error}}",
|
"error_update": "Error: {{error}}",
|
||||||
@@ -261,6 +281,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "Access PIN",
|
"access_pin": "Access PIN",
|
||||||
"add_contact": "Add Contact",
|
"add_contact": "Add Contact",
|
||||||
|
"contact": "Contact",
|
||||||
"create_contact": "Create Contact",
|
"create_contact": "Create Contact",
|
||||||
"currently_selected_contact": "Currently Selected Contact: {{contact}}",
|
"currently_selected_contact": "Currently Selected Contact: {{contact}}",
|
||||||
"delete": "Delete Contact?",
|
"delete": "Delete Contact?",
|
||||||
@@ -300,12 +321,23 @@
|
|||||||
"healthchecks_title": "Delete Healthchecks"
|
"healthchecks_title": "Delete Healthchecks"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Add Device To Blacklist",
|
||||||
|
"all_devices": "All Devices",
|
||||||
|
"blacklisted_on": "Date",
|
||||||
|
"capabilities": "Capabilities",
|
||||||
"certificate_explanation": "Certificates of connected devices",
|
"certificate_explanation": "Certificates of connected devices",
|
||||||
|
"edit_blacklist": "Edit Blacklisted Device",
|
||||||
|
"error_adding_blacklist": "Error adding device to blacklist: {{error}}",
|
||||||
|
"error_edit_blacklist": "Error editing blacklist: {{error}}",
|
||||||
"error_fetching_device": "Error fetching device information: {{error}}",
|
"error_fetching_device": "Error fetching device information: {{error}}",
|
||||||
"error_fetching_devices": "Error while fetching devices: {{error}}",
|
"error_fetching_devices": "Error while fetching devices: {{error}}",
|
||||||
"health_explanation": "Health of connected devices",
|
"health_explanation": "Health of connected devices ((Devices=100% * 100 + Devices>90% * 95 + Devices>60% * 75 + Devices<60% * 35) / ConnectedDevices)",
|
||||||
"memory_explanation": "Memory used by connected devices",
|
"memory_explanation": "Amount of connected devices with corresponding memory used percentage",
|
||||||
"uptimes_explanation": "Time connected devices have been up and connected"
|
"remove_from_blacklist": "Remove from blacklist",
|
||||||
|
"success_added_blacklist": "Device successfully added to blacklist!",
|
||||||
|
"success_edit_blacklist": "Successfully edited blacklist!",
|
||||||
|
"success_removed_blacklist": "Successfully removed device from blacklist!",
|
||||||
|
"uptimes_explanation": "Amount of devices connected based on their uptime"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
@@ -320,23 +352,32 @@
|
|||||||
"add_success": "Entity Successfully Created!",
|
"add_success": "Entity Successfully Created!",
|
||||||
"assigned_inventory": "Assigned Inventory",
|
"assigned_inventory": "Assigned Inventory",
|
||||||
"cannot_delete": "You cannot delete entities which have children. Delete this entity's children to be able to delete it.",
|
"cannot_delete": "You cannot delete entities which have children. Delete this entity's children to be able to delete it.",
|
||||||
|
"confirm_map_delete": "Are you sure you want to delete the map {{name}}? This action cannot be reverted",
|
||||||
"currently_selected_entity": "Currently Selected Entity: {{config}}",
|
"currently_selected_entity": "Currently Selected Entity: {{config}}",
|
||||||
"currently_selected_venue": "Currently Selected Venue: {{config}}",
|
"currently_selected_venue": "Currently Selected Venue: {{config}}",
|
||||||
"delete_success": "Entity Successfully Deleted",
|
"delete_success": "Entity Successfully Deleted",
|
||||||
"delete_warning": "Warning: this operation cannot be reverted",
|
"delete_warning": "Warning: this operation cannot be reverted",
|
||||||
|
"duplicate_from_node": "Duplicate with specific Root Node",
|
||||||
|
"duplicate_map": "Duplicate Map",
|
||||||
|
"duplicate_with_node": "Duplicate {{mapName}} with {{rootName}} as root node",
|
||||||
"edit_failure": "Update unsuccessful : {{error}}",
|
"edit_failure": "Update unsuccessful : {{error}}",
|
||||||
"enter_here": "Enter the IP(s) you'd like to add here",
|
"enter_here": "Enter the IP(s) you'd like to add here",
|
||||||
"entire_tree": "Site Map",
|
"entire_tree": "Network Map",
|
||||||
"entities": "Entities",
|
"entities": "Entities",
|
||||||
"entity": "Entity",
|
"entity": "Entity",
|
||||||
|
"error_deleting_map": "Error deleting map: {{error}}",
|
||||||
"error_fetch_entity": "Error while fetching entity information",
|
"error_fetch_entity": "Error while fetching entity information",
|
||||||
"error_fetching": "Error while fetching entities",
|
"error_fetching": "Error while fetching entities",
|
||||||
"error_fetching_map": "Error fetching map: {{error}}",
|
"error_fetching_map": "Error fetching map: {{error}}",
|
||||||
|
"error_fetching_tree": "Error while fetching tree: {{error}}",
|
||||||
"error_saving": "Error while saving entity",
|
"error_saving": "Error while saving entity",
|
||||||
|
"error_saving_map": "Error saving map: {{error}}",
|
||||||
"higher_priority": "Make Higher Priority",
|
"higher_priority": "Make Higher Priority",
|
||||||
"ip_detection": "IP Detection",
|
"ip_detection": "IP Detection",
|
||||||
"ip_formats": "You can add IPv4 or IPv6 addresses in the following formats:",
|
"ip_formats": "You can add IPv4 or IPv6 addresses in the following formats:",
|
||||||
"lower_priority": "Make Lower Priority",
|
"lower_priority": "Make Lower Priority",
|
||||||
|
"map": "Map",
|
||||||
|
"map_delete_success": "Map Successfully Deleted!",
|
||||||
"need_select_entity": "You need to select an entity from the table below",
|
"need_select_entity": "You need to select an entity from the table below",
|
||||||
"no_ips": "No IPs selected",
|
"no_ips": "No IPs selected",
|
||||||
"not_assigned": "Not Assigned",
|
"not_assigned": "Not Assigned",
|
||||||
@@ -344,6 +385,7 @@
|
|||||||
"select_entity": "Select this Entity",
|
"select_entity": "Select this Entity",
|
||||||
"selected_entity": "Selected Entity",
|
"selected_entity": "Selected Entity",
|
||||||
"selected_map": "Selected Map",
|
"selected_map": "Selected Map",
|
||||||
|
"tree_saved": "Map Successfully Saved!",
|
||||||
"update_failure_error": "Error while trying to update entity: {{error}}",
|
"update_failure_error": "Error while trying to update entity: {{error}}",
|
||||||
"valid_serial": "Needs to be a valid serial number (12 HEX characters)",
|
"valid_serial": "Needs to be a valid serial number (12 HEX characters)",
|
||||||
"venues": "Venues"
|
"venues": "Venues"
|
||||||
@@ -541,6 +583,9 @@
|
|||||||
"verification_code": "Enter your verification here",
|
"verification_code": "Enter your verification here",
|
||||||
"wrong_code": "The verification code that was entered is not valid. "
|
"wrong_code": "The verification code that was entered is not valid. "
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Provisioning"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "When would you like to reboot this device?",
|
"directions": "When would you like to reboot this device?",
|
||||||
"now": "Would you like to reboot this device now?",
|
"now": "Would you like to reboot this device now?",
|
||||||
@@ -551,7 +596,7 @@
|
|||||||
"channel": "Channel",
|
"channel": "Channel",
|
||||||
"directions": "Launch a wifi scan of this device, which should take approximately 25 seconds.",
|
"directions": "Launch a wifi scan of this device, which should take approximately 25 seconds.",
|
||||||
"re_scan": "Re-Scan",
|
"re_scan": "Re-Scan",
|
||||||
"result_directions": "Please click the '$t(scan.re_scan)' button if you would like to do a scan with the same configuration as the last.",
|
"result_directions": "You can click the 'Scan' button at the top right to $t(scan.re_scan)",
|
||||||
"results": "Wi-Fi Scan Results",
|
"results": "Wi-Fi Scan Results",
|
||||||
"scan": "Scan",
|
"scan": "Scan",
|
||||||
"scanning": "Scanning... ",
|
"scanning": "Scanning... ",
|
||||||
@@ -584,7 +629,7 @@
|
|||||||
"mac_prefix": "MAC Prefix",
|
"mac_prefix": "MAC Prefix",
|
||||||
"max_associations": "Max. Associations",
|
"max_associations": "Max. Associations",
|
||||||
"max_clients": "Max. Clients",
|
"max_clients": "Max. Clients",
|
||||||
"messages_transmitted": "Messages Transmitted",
|
"messages_transmitted": "Msgs TX",
|
||||||
"min_associations": "Min. Associations",
|
"min_associations": "Min. Associations",
|
||||||
"min_clients": "Min. Clients",
|
"min_clients": "Min. Clients",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
@@ -592,7 +637,7 @@
|
|||||||
"prefix_length": "Required, needs to be of a length of 6 characters",
|
"prefix_length": "Required, needs to be of a length of 6 characters",
|
||||||
"previous_runs": "Previous Runs",
|
"previous_runs": "Previous Runs",
|
||||||
"received": "Received",
|
"received": "Received",
|
||||||
"received_messages": "Messages Received",
|
"received_messages": "Msgs RX",
|
||||||
"reconnect_interval": "Reconnect Interval",
|
"reconnect_interval": "Reconnect Interval",
|
||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
"resume_success": "Run Resumed!",
|
"resume_success": "Run Resumed!",
|
||||||
@@ -615,11 +660,14 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"data": "Data (KB)",
|
"data": "Data (KB)",
|
||||||
|
"data_mb": "Data (MB)",
|
||||||
"latest_statistics": "Latest Statistics",
|
"latest_statistics": "Latest Statistics",
|
||||||
"lifetime_stats": "Lifetime Statistics",
|
"lifetime_stats": "Lifetime Statistics",
|
||||||
|
"memory": "Memory",
|
||||||
"no_interfaces": "No interface lifetime statistics available",
|
"no_interfaces": "No interface lifetime statistics available",
|
||||||
"show_latest": "Last Statistics",
|
"show_latest": "Last Statistics",
|
||||||
"title": "Statistics"
|
"title": "Statistics",
|
||||||
|
"used": "Used Memory %"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connection_status": "Status",
|
"connection_status": "Status",
|
||||||
@@ -631,9 +679,27 @@
|
|||||||
"percentage_free": "{{percentage}}% of {{total}} free",
|
"percentage_free": "{{percentage}}% of {{total}} free",
|
||||||
"percentage_used": "{{percentage}}% of {{total}} used",
|
"percentage_used": "{{percentage}}% of {{total}} used",
|
||||||
"title": "#{{serialNumber}} Status",
|
"title": "#{{serialNumber}} Status",
|
||||||
|
"total_memory": "Total Memory",
|
||||||
"uptime": "Uptime",
|
"uptime": "Uptime",
|
||||||
"used_total_memory": "{{used}} used / {{total}} total "
|
"used_total_memory": "{{used}} used / {{total}} total "
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"add_device_subscriber_explanation": "To claim devices, you can use our search bar or claim directly from the table. If a device was already claimed by a user, you will need to go to to their details and unassign it before claiming it.",
|
||||||
|
"create": "Create Subscriber",
|
||||||
|
"devices_one": "{{count}} Device",
|
||||||
|
"devices_other": "{{count}} Devices",
|
||||||
|
"edit": "Edit Subscriber",
|
||||||
|
"error_create": "Error creating subscriber: {{error}}",
|
||||||
|
"error_delete": "Error deleting subscriber: {{error}}",
|
||||||
|
"error_fetching": "Error fetching subscribers: {{error}}",
|
||||||
|
"error_fetching_single": "Error fetching subscriber: {{error}}",
|
||||||
|
"error_update": "Error updating subscriber: {{error}}",
|
||||||
|
"is_already_claimed": "is already claimed by ",
|
||||||
|
"subscribers": "Subscribers",
|
||||||
|
"success_create": "Subscriber successfully created!",
|
||||||
|
"success_delete": "Subscriber successfully deleted!",
|
||||||
|
"success_update": "Successfully updated subscriber!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Error while fetching system information",
|
"error_fetching": "Error while fetching system information",
|
||||||
"error_reloading": "Error while reloading: {{error}}",
|
"error_reloading": "Error while reloading: {{error}}",
|
||||||
@@ -723,6 +789,7 @@
|
|||||||
"send_code_again": "Send Code Again",
|
"send_code_again": "Send Code Again",
|
||||||
"show_hide_password": "Show/Hide Password",
|
"show_hide_password": "Show/Hide Password",
|
||||||
"successful_validation": "Phone Number Validated! Click the save button to link it to your profile",
|
"successful_validation": "Phone Number Validated! Click the save button to link it to your profile",
|
||||||
|
"table_title": "Admin Users",
|
||||||
"update_failure": "Error while trying to update: {{error}}",
|
"update_failure": "Error while trying to update: {{error}}",
|
||||||
"update_failure_title": "Update Failed",
|
"update_failure_title": "Update Failed",
|
||||||
"update_success": "User Updated Successfully",
|
"update_success": "User Updated Successfully",
|
||||||
@@ -738,7 +805,10 @@
|
|||||||
"associations": "Associations",
|
"associations": "Associations",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
"network_diagram": "Network Diagram",
|
"network_diagram": "Network Diagram",
|
||||||
|
"override_dfs": "Override DFS",
|
||||||
"radios": "Radios",
|
"radios": "Radios",
|
||||||
"title": "Wi-Fi Analysis"
|
"scan_warning": "Your 5G radio is on a radar channel, you must enable “Override DFS” to allow scanning of all 5G channels",
|
||||||
|
"title": "Wi-Fi Analysis",
|
||||||
|
"vendor": "Vendor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Parpadeo",
|
"blink": "Parpadeo",
|
||||||
"device_leds": "LED de dispositivo",
|
"device_leds": "LED de dispositivo",
|
||||||
"execute_now": "¿Le gustaría establecer este patrón ahora?",
|
"execute_now": "¿Le gustaría establecer este patrón ahora?",
|
||||||
|
"explanation": "¿Qué patrón le gustaría establecer en este dispositivo durante 30 segundos?",
|
||||||
"pattern": "Elija el patrón que le gustaría usar:",
|
"pattern": "Elija el patrón que le gustaría usar:",
|
||||||
"set_leds": "Establecer LED",
|
"set_leds": "Establecer LED",
|
||||||
"when_blink_leds": "¿Cuándo desea que los LED del dispositivo parpadeen?"
|
"when_blink_leds": "¿Cuándo desea que los LED del dispositivo parpadeen?"
|
||||||
@@ -37,13 +38,16 @@
|
|||||||
"add_note": "Añadir la nota",
|
"add_note": "Añadir la nota",
|
||||||
"add_note_explanation": "Escriba su nueva nota a continuación y haga clic en el botón '+' donde haya terminado",
|
"add_note_explanation": "Escriba su nueva nota a continuación y haga clic en el botón '+' donde haya terminado",
|
||||||
"adding_ellipsis": "Añadiendo ...",
|
"adding_ellipsis": "Añadiendo ...",
|
||||||
|
"all": "TODOS",
|
||||||
"are_you_sure": "¿Estás seguro?",
|
"are_you_sure": "¿Estás seguro?",
|
||||||
"back_to_login": "Atrás para iniciar sesión",
|
"back_to_login": "Atrás para iniciar sesión",
|
||||||
"back_to_start": "volver a empezar",
|
"back_to_start": "volver a empezar",
|
||||||
|
"blacklist": "Lista negra",
|
||||||
"by": "Por",
|
"by": "Por",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"certificate": "Certificado",
|
"certificate": "Certificado",
|
||||||
"certificates": "Certificados",
|
"certificates": "Certificados",
|
||||||
|
"claim": "Reclamación",
|
||||||
"clear": "Claro",
|
"clear": "Claro",
|
||||||
"close": "Cerrar",
|
"close": "Cerrar",
|
||||||
"code": "Código",
|
"code": "Código",
|
||||||
@@ -58,16 +62,19 @@
|
|||||||
"confirm_stop_editing": "¿Estás seguro de que quieres dejar de editar? Esto cancelará cualquier cambio no guardado que haya realizado.",
|
"confirm_stop_editing": "¿Estás seguro de que quieres dejar de editar? Esto cancelará cualquier cambio no guardado que haya realizado.",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
"copied": "Copiado!",
|
"copied": "Copiado!",
|
||||||
|
"copied_to_clipboard": "¡Copiado al portapapeles!",
|
||||||
"copy_to_clipboard": "Copiar al portapapeles",
|
"copy_to_clipboard": "Copiar al portapapeles",
|
||||||
"create": "Crear",
|
"create": "Crear",
|
||||||
"created": "creado",
|
"created": "creado",
|
||||||
"created_by": "Creado por",
|
"created_by": "Creado por",
|
||||||
|
"creator": "Creador",
|
||||||
"current": "Corriente",
|
"current": "Corriente",
|
||||||
"custom_date": "Fecha personalizada",
|
"custom_date": "Fecha personalizada",
|
||||||
"dashboard": "Tablero",
|
"dashboard": "Tablero",
|
||||||
"date": "Fecha",
|
"date": "Fecha",
|
||||||
"day": "día",
|
"day": "día",
|
||||||
"days": "días",
|
"days": "días",
|
||||||
|
"default_map": "Mapa predeterminado",
|
||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
"delete_device": "Eliminar dispositivo",
|
"delete_device": "Eliminar dispositivo",
|
||||||
"details": "Detalles",
|
"details": "Detalles",
|
||||||
@@ -85,6 +92,7 @@
|
|||||||
"dismiss": "Despedir",
|
"dismiss": "Despedir",
|
||||||
"do_now": "¡Hagan ahora!",
|
"do_now": "¡Hagan ahora!",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
|
"duplicate": "Duplicar",
|
||||||
"duration": "Duración",
|
"duration": "Duración",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"edit_user": "Editar",
|
"edit_user": "Editar",
|
||||||
@@ -94,6 +102,7 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_adding_note": "Error al agregar una nota",
|
"error_adding_note": "Error al agregar una nota",
|
||||||
"error_code": "código de error",
|
"error_code": "código de error",
|
||||||
|
"errors": "Los errores",
|
||||||
"execute_now": "¿Le gustaría ejecutar este comando ahora?",
|
"execute_now": "¿Le gustaría ejecutar este comando ahora?",
|
||||||
"executed": "ejecutado",
|
"executed": "ejecutado",
|
||||||
"exit": "salida",
|
"exit": "salida",
|
||||||
@@ -110,6 +119,7 @@
|
|||||||
"hours": "horas",
|
"hours": "horas",
|
||||||
"id": "Carné de identidad",
|
"id": "Carné de identidad",
|
||||||
"invalid_credentials": "Nombre de usuario y / o contraseña inválido",
|
"invalid_credentials": "Nombre de usuario y / o contraseña inválido",
|
||||||
|
"invalid_date_explanation": "Fecha no válida, utilice el calendario accesible con el botón de la derecha",
|
||||||
"invalid_file": "El archivo elegido no es válido, lea las instrucciones y ajuste su archivo en consecuencia",
|
"invalid_file": "El archivo elegido no es válido, lea las instrucciones y ajuste su archivo en consecuencia",
|
||||||
"invalid_password": "Esta contraseña no confirma las reglas básicas de contraseña. Visite nuestra página de Política de contraseñas para obtener más información.",
|
"invalid_password": "Esta contraseña no confirma las reglas básicas de contraseña. Visite nuestra página de Política de contraseñas para obtener más información.",
|
||||||
"invalid_pem": "Su archivo .pem no es válido. Debe comenzar con '----- BEGIN CERTIFICATE -----' O '----- BEGIN PRIVATE KEY -----' y debe terminar con '----- END CERTIFICATE --- - 'O' ----- FIN DE CLAVE PRIVADA ----- '",
|
"invalid_pem": "Su archivo .pem no es válido. Debe comenzar con '----- BEGIN CERTIFICATE -----' O '----- BEGIN PRIVATE KEY -----' y debe terminar con '----- END CERTIFICATE --- - 'O' ----- FIN DE CLAVE PRIVADA ----- '",
|
||||||
@@ -142,17 +152,20 @@
|
|||||||
"no_items": "No hay articulos",
|
"no_items": "No hay articulos",
|
||||||
"none": "Ninguna",
|
"none": "Ninguna",
|
||||||
"not_connected": "No conectado",
|
"not_connected": "No conectado",
|
||||||
"of_connected": "% de dispositivos",
|
"of_connected": "% de dispositivos conectados",
|
||||||
"off": "Apagado",
|
"off": "Apagado",
|
||||||
"on": "en",
|
"on": "en",
|
||||||
"optional": "Opcional",
|
"optional": "Opcional",
|
||||||
"overall_health": "Salud en general",
|
"overall_health": "Salud en general",
|
||||||
"password_policy": "Política de contraseñas",
|
"password_policy": "Política de contraseñas",
|
||||||
|
"preferences": "Preferencias",
|
||||||
"preview": "Avance",
|
"preview": "Avance",
|
||||||
"program": "Programa",
|
"program": "Programa",
|
||||||
|
"reason": "Razón",
|
||||||
"recorded": "Grabado",
|
"recorded": "Grabado",
|
||||||
"refresh": "Refrescar",
|
"refresh": "Refrescar",
|
||||||
"refresh_device": "Actualizar dispositivo",
|
"refresh_device": "Actualizar dispositivo",
|
||||||
|
"remove_claim": "Quitar reclamo",
|
||||||
"required": "Necesario",
|
"required": "Necesario",
|
||||||
"result": "Resultado",
|
"result": "Resultado",
|
||||||
"save": "Salvar",
|
"save": "Salvar",
|
||||||
@@ -170,12 +183,14 @@
|
|||||||
"show_all": "Mostrar todo",
|
"show_all": "Mostrar todo",
|
||||||
"socket_connection_closed": "¡Conexión cerrada!",
|
"socket_connection_closed": "¡Conexión cerrada!",
|
||||||
"start": "comienzo",
|
"start": "comienzo",
|
||||||
|
"status": "Estado",
|
||||||
"stop_editing": "Dejar de editar",
|
"stop_editing": "Dejar de editar",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"submitted": "Presentado",
|
"submitted": "Presentado",
|
||||||
"success": "Éxito",
|
"success": "Éxito",
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"table": "Mesa",
|
"table": "Mesa",
|
||||||
|
"time_per_device": "Dispositivo / segundo",
|
||||||
"timestamp": "hora",
|
"timestamp": "hora",
|
||||||
"to": "a",
|
"to": "a",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
@@ -190,6 +205,8 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendedores",
|
"vendors": "Vendedores",
|
||||||
"view_more": "Ver más",
|
"view_more": "Ver más",
|
||||||
|
"visibility": "Visibilidad",
|
||||||
|
"waiting_for_update": "Esperando actualización",
|
||||||
"yes": "Sí"
|
"yes": "Sí"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +227,8 @@
|
|||||||
"creation_success": "¡Configuración creada con éxito!",
|
"creation_success": "¡Configuración creada con éxito!",
|
||||||
"currently_associated": "Configuración asociada actual: {{config}}",
|
"currently_associated": "Configuración asociada actual: {{config}}",
|
||||||
"currently_selected_config": "Configuración seleccionada actualmente: {{config}}",
|
"currently_selected_config": "Configuración seleccionada actualmente: {{config}}",
|
||||||
|
"default_configs": "Configuraciones predeterminadas",
|
||||||
|
"default_configurations": "Configuraciones predeterminadas",
|
||||||
"delete_config": "Eliminar Configuración",
|
"delete_config": "Eliminar Configuración",
|
||||||
"details": "Detalles",
|
"details": "Detalles",
|
||||||
"device_password": "Contraseña",
|
"device_password": "Contraseña",
|
||||||
@@ -218,6 +237,7 @@
|
|||||||
"devices_affected": "Dispositivos afectados por esta configuración:",
|
"devices_affected": "Dispositivos afectados por esta configuración:",
|
||||||
"edit_configuration": "Editar configuración",
|
"edit_configuration": "Editar configuración",
|
||||||
"error_delete": "Error al intentar eliminar: {{error}}",
|
"error_delete": "Error al intentar eliminar: {{error}}",
|
||||||
|
"error_delete_blacklist": "Error al eliminar de la lista negra: {{error}}",
|
||||||
"error_fetching_config": "Error al obtener la configuración",
|
"error_fetching_config": "Error al obtener la configuración",
|
||||||
"error_trying_delete": "Error al intentar eliminar: {{error}}",
|
"error_trying_delete": "Error al intentar eliminar: {{error}}",
|
||||||
"error_update": "Error: {{error}}",
|
"error_update": "Error: {{error}}",
|
||||||
@@ -261,6 +281,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "PIN de acceso",
|
"access_pin": "PIN de acceso",
|
||||||
"add_contact": "Agregar contacto",
|
"add_contact": "Agregar contacto",
|
||||||
|
"contact": "Contacto",
|
||||||
"create_contact": "Crear contacto",
|
"create_contact": "Crear contacto",
|
||||||
"currently_selected_contact": "Contacto seleccionado actualmente: {{contact}}",
|
"currently_selected_contact": "Contacto seleccionado actualmente: {{contact}}",
|
||||||
"delete": "¿Borrar contacto?",
|
"delete": "¿Borrar contacto?",
|
||||||
@@ -300,12 +321,23 @@
|
|||||||
"healthchecks_title": "Eliminar comprobaciones de estado"
|
"healthchecks_title": "Eliminar comprobaciones de estado"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Agregar dispositivo a la lista negra",
|
||||||
|
"all_devices": "Todos los dispositivos",
|
||||||
|
"blacklisted_on": "Fecha",
|
||||||
|
"capabilities": "capacidades",
|
||||||
"certificate_explanation": "Certificados de dispositivos conectados",
|
"certificate_explanation": "Certificados de dispositivos conectados",
|
||||||
|
"edit_blacklist": "Editar dispositivo incluido en la lista negra",
|
||||||
|
"error_adding_blacklist": "Error al agregar el dispositivo a la lista negra: {{error}}",
|
||||||
|
"error_edit_blacklist": "Error al editar la lista negra: {{error}}",
|
||||||
"error_fetching_device": "Error al obtener la información del dispositivo: {{error}}",
|
"error_fetching_device": "Error al obtener la información del dispositivo: {{error}}",
|
||||||
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}",
|
"error_fetching_devices": "Error al recuperar dispositivos: {{error}}",
|
||||||
"health_explanation": "Salud de los dispositivos conectados",
|
"health_explanation": "Estado de los dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos conectados)",
|
||||||
"memory_explanation": "Memoria utilizada por dispositivos conectados",
|
"memory_explanation": "Cantidad de dispositivos conectados con la memoria correspondiente utilizada%",
|
||||||
"uptimes_explanation": "Tiempo que los dispositivos conectados han estado en funcionamiento y conectados"
|
"remove_from_blacklist": "ELIMINAR DE LA LISTA NEGRA",
|
||||||
|
"success_added_blacklist": "¡Dispositivo agregado exitosamente a la lista negra!",
|
||||||
|
"success_edit_blacklist": "Lista negra editada con éxito!",
|
||||||
|
"success_removed_blacklist": "¡Dispositivo eliminado con éxito de la lista negra!",
|
||||||
|
"uptimes_explanation": "Cantidad de dispositivos conectados según su tiempo de actividad"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Iniciar sesión",
|
"log": "Iniciar sesión",
|
||||||
@@ -320,23 +352,32 @@
|
|||||||
"add_success": "¡Entidad creada con éxito!",
|
"add_success": "¡Entidad creada con éxito!",
|
||||||
"assigned_inventory": "Inventario asignado",
|
"assigned_inventory": "Inventario asignado",
|
||||||
"cannot_delete": "No puede eliminar entidades que tienen hijos. Elimina los hijos de esta entidad para poder eliminarla.",
|
"cannot_delete": "No puede eliminar entidades que tienen hijos. Elimina los hijos de esta entidad para poder eliminarla.",
|
||||||
|
"confirm_map_delete": "¿Está seguro de que desea eliminar el mapa {{name}}? Esta acción no se puede revertir",
|
||||||
"currently_selected_entity": "Entidad seleccionada actualmente: {{config}}",
|
"currently_selected_entity": "Entidad seleccionada actualmente: {{config}}",
|
||||||
"currently_selected_venue": "Lugar seleccionado actualmente: {{config}}",
|
"currently_selected_venue": "Lugar seleccionado actualmente: {{config}}",
|
||||||
"delete_success": "Entidad eliminada correctamente",
|
"delete_success": "Entidad eliminada correctamente",
|
||||||
"delete_warning": "Advertencia: esta operación no se puede revertir",
|
"delete_warning": "Advertencia: esta operación no se puede revertir",
|
||||||
|
"duplicate_from_node": "Duplicar con un nodo raíz específico",
|
||||||
|
"duplicate_map": "Mapa duplicado",
|
||||||
|
"duplicate_with_node": "Duplicar {{mapName}} con {{rootName}} como nodo raíz",
|
||||||
"edit_failure": "Actualización fallida: {{error}}",
|
"edit_failure": "Actualización fallida: {{error}}",
|
||||||
"enter_here": "Ingrese las IP que desea agregar aquí",
|
"enter_here": "Ingrese las IP que desea agregar aquí",
|
||||||
"entire_tree": "Site MAp",
|
"entire_tree": "Mapa de red",
|
||||||
"entities": "entidades",
|
"entities": "entidades",
|
||||||
"entity": "Entidad",
|
"entity": "Entidad",
|
||||||
|
"error_deleting_map": "Error al eliminar el mapa: {{error}}",
|
||||||
"error_fetch_entity": "Error al obtener la información de la entidad",
|
"error_fetch_entity": "Error al obtener la información de la entidad",
|
||||||
"error_fetching": "Error al recuperar entidades",
|
"error_fetching": "Error al recuperar entidades",
|
||||||
"error_fetching_map": "Error al obtener el mapa: {{error}}",
|
"error_fetching_map": "Error al obtener el mapa: {{error}}",
|
||||||
|
"error_fetching_tree": "Error al obtener el árbol: {{error}}",
|
||||||
"error_saving": "Error al guardar la entidad",
|
"error_saving": "Error al guardar la entidad",
|
||||||
|
"error_saving_map": "Error al guardar el mapa: {{error}}",
|
||||||
"higher_priority": "Dar mayor prioridad",
|
"higher_priority": "Dar mayor prioridad",
|
||||||
"ip_detection": "Detección de IP",
|
"ip_detection": "Detección de IP",
|
||||||
"ip_formats": "Puede agregar direcciones IPv4 o IPv6 en los siguientes formatos:",
|
"ip_formats": "Puede agregar direcciones IPv4 o IPv6 en los siguientes formatos:",
|
||||||
"lower_priority": "Hacer una prioridad más baja",
|
"lower_priority": "Hacer una prioridad más baja",
|
||||||
|
"map": "Mapa",
|
||||||
|
"map_delete_success": "¡Mapa eliminado correctamente!",
|
||||||
"need_select_entity": "Debe seleccionar una entidad de la siguiente tabla",
|
"need_select_entity": "Debe seleccionar una entidad de la siguiente tabla",
|
||||||
"no_ips": "No se seleccionaron direcciones IP",
|
"no_ips": "No se seleccionaron direcciones IP",
|
||||||
"not_assigned": "No asignado",
|
"not_assigned": "No asignado",
|
||||||
@@ -344,6 +385,7 @@
|
|||||||
"select_entity": "Seleccione esta entidad",
|
"select_entity": "Seleccione esta entidad",
|
||||||
"selected_entity": "Entidad seleccionada",
|
"selected_entity": "Entidad seleccionada",
|
||||||
"selected_map": "Mapa seleccionado",
|
"selected_map": "Mapa seleccionado",
|
||||||
|
"tree_saved": "¡Mapa guardado con éxito!",
|
||||||
"update_failure_error": "Error al intentar actualizar la entidad: {{error}}",
|
"update_failure_error": "Error al intentar actualizar la entidad: {{error}}",
|
||||||
"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)",
|
"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)",
|
||||||
"venues": "Sedes"
|
"venues": "Sedes"
|
||||||
@@ -541,6 +583,9 @@
|
|||||||
"verification_code": "Ingrese su verificación aquí",
|
"verification_code": "Ingrese su verificación aquí",
|
||||||
"wrong_code": "El código de verificación que se ingresó no es válido."
|
"wrong_code": "El código de verificación que se ingresó no es válido."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Aprovisionamiento"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "¿Cuándo le gustaría reiniciar este dispositivo?",
|
"directions": "¿Cuándo le gustaría reiniciar este dispositivo?",
|
||||||
"now": "¿Le gustaría reiniciar este dispositivo ahora?",
|
"now": "¿Le gustaría reiniciar este dispositivo ahora?",
|
||||||
@@ -551,7 +596,7 @@
|
|||||||
"channel": "Canal",
|
"channel": "Canal",
|
||||||
"directions": "Ejecute un escaneo wifi de este dispositivo, que debería tomar aproximadamente 25 segundos.",
|
"directions": "Ejecute un escaneo wifi de este dispositivo, que debería tomar aproximadamente 25 segundos.",
|
||||||
"re_scan": "Vuelva a escanear",
|
"re_scan": "Vuelva a escanear",
|
||||||
"result_directions": "Haga clic en el botón '$ t (scan.re_scan)' si desea realizar un escaneo con la misma configuración que el anterior.",
|
"result_directions": "Puede hacer clic en el botón 'Escanear' en la parte superior derecha para $t(scan.re_scan)",
|
||||||
"results": "Resultados de escaneo Wi-Fi",
|
"results": "Resultados de escaneo Wi-Fi",
|
||||||
"scan": "Escanear",
|
"scan": "Escanear",
|
||||||
"scanning": "Exploración... ",
|
"scanning": "Exploración... ",
|
||||||
@@ -584,7 +629,7 @@
|
|||||||
"mac_prefix": "Prefijo MAC",
|
"mac_prefix": "Prefijo MAC",
|
||||||
"max_associations": "Max. Asociaciones",
|
"max_associations": "Max. Asociaciones",
|
||||||
"max_clients": "Max. Clientela",
|
"max_clients": "Max. Clientela",
|
||||||
"messages_transmitted": "Mensajes transmitidos",
|
"messages_transmitted": "Mensajes TX",
|
||||||
"min_associations": "Min. Asociaciones",
|
"min_associations": "Min. Asociaciones",
|
||||||
"min_clients": "Min. Clientela",
|
"min_clients": "Min. Clientela",
|
||||||
"pause": "pausa",
|
"pause": "pausa",
|
||||||
@@ -592,7 +637,7 @@
|
|||||||
"prefix_length": "Obligatorio, debe tener una longitud de 6 caracteres",
|
"prefix_length": "Obligatorio, debe tener una longitud de 6 caracteres",
|
||||||
"previous_runs": "Ejecuciones anteriores",
|
"previous_runs": "Ejecuciones anteriores",
|
||||||
"received": "recibido",
|
"received": "recibido",
|
||||||
"received_messages": "Mensajes recibidos",
|
"received_messages": "Msgs RX",
|
||||||
"reconnect_interval": "Intervalo de reconexión",
|
"reconnect_interval": "Intervalo de reconexión",
|
||||||
"resume": "Currículum",
|
"resume": "Currículum",
|
||||||
"resume_success": "¡Ejecutar reanudado!",
|
"resume_success": "¡Ejecutar reanudado!",
|
||||||
@@ -615,11 +660,14 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"data": "Datos (KB)",
|
"data": "Datos (KB)",
|
||||||
|
"data_mb": "Datos (MB)",
|
||||||
"latest_statistics": "Últimas estadísticas",
|
"latest_statistics": "Últimas estadísticas",
|
||||||
"lifetime_stats": "Estadísticas de por vida",
|
"lifetime_stats": "Estadísticas de por vida",
|
||||||
|
"memory": "Memoria",
|
||||||
"no_interfaces": "No hay estadísticas de vida útil de la interfaz disponibles",
|
"no_interfaces": "No hay estadísticas de vida útil de la interfaz disponibles",
|
||||||
"show_latest": "Últimas estadísticas",
|
"show_latest": "Últimas estadísticas",
|
||||||
"title": "estadística"
|
"title": "estadística",
|
||||||
|
"used": "Memoria usada %"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connection_status": "Estado",
|
"connection_status": "Estado",
|
||||||
@@ -631,9 +679,27 @@
|
|||||||
"percentage_free": "{{percentage}}% de {{total}} gratis",
|
"percentage_free": "{{percentage}}% de {{total}} gratis",
|
||||||
"percentage_used": "{{percentage}}% de {{total}} utilizado",
|
"percentage_used": "{{percentage}}% de {{total}} utilizado",
|
||||||
"title": "#{{serialNumber}} Estado",
|
"title": "#{{serialNumber}} Estado",
|
||||||
|
"total_memory": "Memoria total",
|
||||||
"uptime": "Tiempo de actividad",
|
"uptime": "Tiempo de actividad",
|
||||||
"used_total_memory": "{{used}} usado / {{total}} total"
|
"used_total_memory": "{{used}} usado / {{total}} total"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"add_device_subscriber_explanation": "Para reclamar otros dispositivos, puede usar nuestra barra de búsqueda o reclamar directamente desde la tabla. Si un dispositivo ya fue reclamado por un usuario, deberá ir a sus detalles y anular la asignación antes de reclamarlo.",
|
||||||
|
"create": "Crear suscriptor",
|
||||||
|
"devices_one": "{{count}} dispositivo",
|
||||||
|
"devices_other": "{{count}} dispositivos",
|
||||||
|
"edit": "Editar suscriptor",
|
||||||
|
"error_create": "Error al crear el suscriptor: {{error}}",
|
||||||
|
"error_delete": "Error al eliminar el suscriptor: {{error}}",
|
||||||
|
"error_fetching": "Error al obtener suscriptores: {{error}}",
|
||||||
|
"error_fetching_single": "Error al obtener el suscriptor: {{error}}",
|
||||||
|
"error_update": "Error al actualizar el suscriptor: {{error}}",
|
||||||
|
"is_already_claimed": "ya es reclamado por",
|
||||||
|
"subscribers": "Suscriptores",
|
||||||
|
"success_create": "¡Suscriptor creado correctamente!",
|
||||||
|
"success_delete": "¡Suscriptor eliminado correctamente!",
|
||||||
|
"success_update": "Suscriptor actualizado con éxito!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Error al obtener información del sistema",
|
"error_fetching": "Error al obtener información del sistema",
|
||||||
"error_reloading": "Error al recargar: {{error}}",
|
"error_reloading": "Error al recargar: {{error}}",
|
||||||
@@ -723,6 +789,7 @@
|
|||||||
"send_code_again": "Enviar Código De nuevo",
|
"send_code_again": "Enviar Código De nuevo",
|
||||||
"show_hide_password": "Mostrar / Ocultar contraseña",
|
"show_hide_password": "Mostrar / Ocultar contraseña",
|
||||||
"successful_validation": "¡Número de teléfono validado! Haga clic en el botón guardar para vincularlo a su perfil",
|
"successful_validation": "¡Número de teléfono validado! Haga clic en el botón guardar para vincularlo a su perfil",
|
||||||
|
"table_title": "Usuarios administrativos",
|
||||||
"update_failure": "Error al intentar actualizar: {{error}}",
|
"update_failure": "Error al intentar actualizar: {{error}}",
|
||||||
"update_failure_title": "Actualización fallida",
|
"update_failure_title": "Actualización fallida",
|
||||||
"update_success": "Usuario actualizado con éxito",
|
"update_success": "Usuario actualizado con éxito",
|
||||||
@@ -738,7 +805,10 @@
|
|||||||
"associations": "Asociaciones",
|
"associations": "Asociaciones",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
"network_diagram": "Diagrama de Red",
|
"network_diagram": "Diagrama de Red",
|
||||||
|
"override_dfs": "Anular DFS",
|
||||||
"radios": "Radios",
|
"radios": "Radios",
|
||||||
"title": "Análisis de Wi-Fi"
|
"scan_warning": "Su radio 5G está en un canal de radar, debe habilitar \"Anular DFS\" para permitir el escaneo de todos los canales 5G",
|
||||||
|
"title": "Análisis de Wi-Fi",
|
||||||
|
"vendor": "Vendedor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Cligner",
|
"blink": "Cligner",
|
||||||
"device_leds": "LED de l'appareil",
|
"device_leds": "LED de l'appareil",
|
||||||
"execute_now": "Souhaitez-vous définir ce modèle maintenant ?",
|
"execute_now": "Souhaitez-vous définir ce modèle maintenant ?",
|
||||||
|
"explanation": "Quel modèle souhaitez-vous définir sur cet appareil pendant 30 secondes ?",
|
||||||
"pattern": "Choisissez le modèle que vous souhaitez utiliser :",
|
"pattern": "Choisissez le modèle que vous souhaitez utiliser :",
|
||||||
"set_leds": "Définir les LED",
|
"set_leds": "Définir les LED",
|
||||||
"when_blink_leds": "Quand souhaitez-vous faire clignoter les LED de l'appareil ?"
|
"when_blink_leds": "Quand souhaitez-vous faire clignoter les LED de l'appareil ?"
|
||||||
@@ -37,13 +38,16 @@
|
|||||||
"add_note": "Ajouter une note",
|
"add_note": "Ajouter une note",
|
||||||
"add_note_explanation": "Écrivez votre nouvelle note ci-dessous et cliquez sur le bouton '+' où vous avez terminé",
|
"add_note_explanation": "Écrivez votre nouvelle note ci-dessous et cliquez sur le bouton '+' où vous avez terminé",
|
||||||
"adding_ellipsis": "Ajouter...",
|
"adding_ellipsis": "Ajouter...",
|
||||||
|
"all": "Tout",
|
||||||
"are_you_sure": "Êtes-vous sûr?",
|
"are_you_sure": "Êtes-vous sûr?",
|
||||||
"back_to_login": "Retour connexion",
|
"back_to_login": "Retour connexion",
|
||||||
"back_to_start": "Retour au début",
|
"back_to_start": "Retour au début",
|
||||||
|
"blacklist": "Liste noire",
|
||||||
"by": "Par",
|
"by": "Par",
|
||||||
"cancel": "annuler",
|
"cancel": "annuler",
|
||||||
"certificate": "Certificat",
|
"certificate": "Certificat",
|
||||||
"certificates": "Certificats",
|
"certificates": "Certificats",
|
||||||
|
"claim": "Prétendre",
|
||||||
"clear": "Clair",
|
"clear": "Clair",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
@@ -58,16 +62,19 @@
|
|||||||
"confirm_stop_editing": "Voulez-vous vraiment arrêter la modification ? Cela annulera toutes les modifications non enregistrées que vous avez apportées.",
|
"confirm_stop_editing": "Voulez-vous vraiment arrêter la modification ? Cela annulera toutes les modifications non enregistrées que vous avez apportées.",
|
||||||
"connected": "Connecté",
|
"connected": "Connecté",
|
||||||
"copied": "Copié!",
|
"copied": "Copié!",
|
||||||
|
"copied_to_clipboard": "Copié dans le presse-papier!",
|
||||||
"copy_to_clipboard": "Copier dans le presse-papier",
|
"copy_to_clipboard": "Copier dans le presse-papier",
|
||||||
"create": "Créer",
|
"create": "Créer",
|
||||||
"created": "Créé",
|
"created": "Créé",
|
||||||
"created_by": "Créé par",
|
"created_by": "Créé par",
|
||||||
|
"creator": "Créateur",
|
||||||
"current": "Actuel",
|
"current": "Actuel",
|
||||||
"custom_date": "Date personnalisée",
|
"custom_date": "Date personnalisée",
|
||||||
"dashboard": "Tableau de bord",
|
"dashboard": "Tableau de bord",
|
||||||
"date": "Rendez-vous amoureux",
|
"date": "Rendez-vous amoureux",
|
||||||
"day": "journée",
|
"day": "journée",
|
||||||
"days": "journées",
|
"days": "journées",
|
||||||
|
"default_map": "Carte par défaut",
|
||||||
"delete": "Effacer",
|
"delete": "Effacer",
|
||||||
"delete_device": "Supprimer le périphérique",
|
"delete_device": "Supprimer le périphérique",
|
||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
@@ -85,6 +92,7 @@
|
|||||||
"dismiss": "Rejeter",
|
"dismiss": "Rejeter",
|
||||||
"do_now": "Faire maintenant!",
|
"do_now": "Faire maintenant!",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
|
"duplicate": "Dupliquer",
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"edit": "modifier",
|
"edit": "modifier",
|
||||||
"edit_user": "Modifier",
|
"edit_user": "Modifier",
|
||||||
@@ -94,6 +102,7 @@
|
|||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"error_adding_note": "Erreur lors de l'ajout de la note",
|
"error_adding_note": "Erreur lors de l'ajout de la note",
|
||||||
"error_code": "Code d'erreur",
|
"error_code": "Code d'erreur",
|
||||||
|
"errors": "les erreurs",
|
||||||
"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?",
|
"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?",
|
||||||
"executed": "réalisé",
|
"executed": "réalisé",
|
||||||
"exit": "Sortie",
|
"exit": "Sortie",
|
||||||
@@ -110,6 +119,7 @@
|
|||||||
"hours": "heures",
|
"hours": "heures",
|
||||||
"id": "Id",
|
"id": "Id",
|
||||||
"invalid_credentials": "Nom d'utilisateur et / ou mot de passe incorrect",
|
"invalid_credentials": "Nom d'utilisateur et / ou mot de passe incorrect",
|
||||||
|
"invalid_date_explanation": "Date invalide, merci d'utiliser le calendrier accessible avec le bouton à droite",
|
||||||
"invalid_file": "Le fichier choisi n'était pas valide, veuillez lire les instructions et ajuster votre fichier en conséquence",
|
"invalid_file": "Le fichier choisi n'était pas valide, veuillez lire les instructions et ajuster votre fichier en conséquence",
|
||||||
"invalid_password": "Ce mot de passe ne confirme pas les règles de base des mots de passe. Veuillez visiter notre page Politique de mot de passe pour en savoir plus",
|
"invalid_password": "Ce mot de passe ne confirme pas les règles de base des mots de passe. Veuillez visiter notre page Politique de mot de passe pour en savoir plus",
|
||||||
"invalid_pem": "Votre fichier .pem n'est pas valide. Il doit commencer par '-----BEGIN CERTIFICATE-----' OU '-----BEGIN PRIVATE KEY-----' et il doit se terminer par '-----END CERTIFICATE--- --' OU '-----FIN CLÉ PRIVÉE-----'",
|
"invalid_pem": "Votre fichier .pem n'est pas valide. Il doit commencer par '-----BEGIN CERTIFICATE-----' OU '-----BEGIN PRIVATE KEY-----' et il doit se terminer par '-----END CERTIFICATE--- --' OU '-----FIN CLÉ PRIVÉE-----'",
|
||||||
@@ -142,17 +152,20 @@
|
|||||||
"no_items": "Pas d'objet",
|
"no_items": "Pas d'objet",
|
||||||
"none": "Aucun",
|
"none": "Aucun",
|
||||||
"not_connected": "Pas connecté",
|
"not_connected": "Pas connecté",
|
||||||
"of_connected": "% d'appareils",
|
"of_connected": "% d'appareils connectés",
|
||||||
"off": "De",
|
"off": "De",
|
||||||
"on": "sur",
|
"on": "sur",
|
||||||
"optional": "Optionnel",
|
"optional": "Optionnel",
|
||||||
"overall_health": "Santé globale",
|
"overall_health": "Santé globale",
|
||||||
"password_policy": "Politique de mot de passe",
|
"password_policy": "Politique de mot de passe",
|
||||||
|
"preferences": "Préférences",
|
||||||
"preview": "Aperçu",
|
"preview": "Aperçu",
|
||||||
"program": "Programme",
|
"program": "Programme",
|
||||||
|
"reason": "raison",
|
||||||
"recorded": "Enregistré",
|
"recorded": "Enregistré",
|
||||||
"refresh": "Rafraîchir",
|
"refresh": "Rafraîchir",
|
||||||
"refresh_device": "Actualiser l'appareil",
|
"refresh_device": "Actualiser l'appareil",
|
||||||
|
"remove_claim": "Supprimer la réclamation",
|
||||||
"required": "Champs obligatoires",
|
"required": "Champs obligatoires",
|
||||||
"result": "Résultat",
|
"result": "Résultat",
|
||||||
"save": "Sauvegarder",
|
"save": "Sauvegarder",
|
||||||
@@ -170,12 +183,14 @@
|
|||||||
"show_all": "Montre tout",
|
"show_all": "Montre tout",
|
||||||
"socket_connection_closed": "Connexion fermée !",
|
"socket_connection_closed": "Connexion fermée !",
|
||||||
"start": "Début",
|
"start": "Début",
|
||||||
|
"status": "Statut",
|
||||||
"stop_editing": "Arrêter la modification",
|
"stop_editing": "Arrêter la modification",
|
||||||
"submit": "Soumettre",
|
"submit": "Soumettre",
|
||||||
"submitted": "Soumis",
|
"submitted": "Soumis",
|
||||||
"success": "Succès",
|
"success": "Succès",
|
||||||
"system": "Système",
|
"system": "Système",
|
||||||
"table": "Table",
|
"table": "Table",
|
||||||
|
"time_per_device": "Appareils/Seconde",
|
||||||
"timestamp": "Temps",
|
"timestamp": "Temps",
|
||||||
"to": "à",
|
"to": "à",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
@@ -190,6 +205,8 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendeurs",
|
"vendors": "Vendeurs",
|
||||||
"view_more": "Afficher plus",
|
"view_more": "Afficher plus",
|
||||||
|
"visibility": "Visibilité",
|
||||||
|
"waiting_for_update": "En attente de mise à jour",
|
||||||
"yes": "Oui"
|
"yes": "Oui"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +227,8 @@
|
|||||||
"creation_success": "Configuration créée avec succès !",
|
"creation_success": "Configuration créée avec succès !",
|
||||||
"currently_associated": "Configuration associée actuelle : {{config}}",
|
"currently_associated": "Configuration associée actuelle : {{config}}",
|
||||||
"currently_selected_config": "Configuration actuellement sélectionnée : {{config}}",
|
"currently_selected_config": "Configuration actuellement sélectionnée : {{config}}",
|
||||||
|
"default_configs": "Configurations par défaut",
|
||||||
|
"default_configurations": "Configurations par défaut",
|
||||||
"delete_config": "Supprimer la configuration",
|
"delete_config": "Supprimer la configuration",
|
||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
"device_password": "Mot de passe",
|
"device_password": "Mot de passe",
|
||||||
@@ -218,6 +237,7 @@
|
|||||||
"devices_affected": "Appareils concernés par cette configuration :",
|
"devices_affected": "Appareils concernés par cette configuration :",
|
||||||
"edit_configuration": "Modifier la configuration",
|
"edit_configuration": "Modifier la configuration",
|
||||||
"error_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
"error_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
||||||
|
"error_delete_blacklist": "Erreur lors de la suppression de la liste noire : {{error}}",
|
||||||
"error_fetching_config": "Erreur lors de la récupération de la configuration",
|
"error_fetching_config": "Erreur lors de la récupération de la configuration",
|
||||||
"error_trying_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
"error_trying_delete": "Erreur lors de la tentative de suppression : {{error}}",
|
||||||
"error_update": "Erreur: {{error}}",
|
"error_update": "Erreur: {{error}}",
|
||||||
@@ -261,6 +281,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "NIP d'accès",
|
"access_pin": "NIP d'accès",
|
||||||
"add_contact": "Ajouter le contact",
|
"add_contact": "Ajouter le contact",
|
||||||
|
"contact": "Contact",
|
||||||
"create_contact": "Créer un contact",
|
"create_contact": "Créer un contact",
|
||||||
"currently_selected_contact": "Contact actuellement sélectionné : {{contact}}",
|
"currently_selected_contact": "Contact actuellement sélectionné : {{contact}}",
|
||||||
"delete": "Effacer le contact?",
|
"delete": "Effacer le contact?",
|
||||||
@@ -300,12 +321,23 @@
|
|||||||
"healthchecks_title": "Supprimer les vérifications d'état"
|
"healthchecks_title": "Supprimer les vérifications d'état"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Ajouter un appareil à la liste noire",
|
||||||
|
"all_devices": "Tous les dispositifs",
|
||||||
|
"blacklisted_on": "Rendez-vous amoureux",
|
||||||
|
"capabilities": "Capacités",
|
||||||
"certificate_explanation": "Certificats des appareils connectés",
|
"certificate_explanation": "Certificats des appareils connectés",
|
||||||
|
"edit_blacklist": "Modifier l'appareil sur liste noire",
|
||||||
|
"error_adding_blacklist": "Erreur lors de l'ajout de l'appareil à la liste noire : {{error}}",
|
||||||
|
"error_edit_blacklist": "Erreur lors de la modification de la liste noire : {{error}}",
|
||||||
"error_fetching_device": "Erreur lors de la récupération des informations sur l'appareil : {{error}}",
|
"error_fetching_device": "Erreur lors de la récupération des informations sur l'appareil : {{error}}",
|
||||||
"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}",
|
"error_fetching_devices": "Erreur lors de la récupération des appareils : {{error}}",
|
||||||
"health_explanation": "Santé des appareils connectés",
|
"health_explanation": "Santé des appareils connectés ((Appareils = 100 % * 100 + Appareils> 90 % * 95 + Appareils> 60 % * 75 + Appareils < 60 % * 35) / Appareils connectés)",
|
||||||
"memory_explanation": "Mémoire utilisée par les appareils connectés",
|
"memory_explanation": "Nombre d'appareils connectés avec la mémoire correspondante utilisée %",
|
||||||
"uptimes_explanation": "Heure à laquelle les appareils connectés ont été activés et connectés"
|
"remove_from_blacklist": "Supprimer de la liste noire",
|
||||||
|
"success_added_blacklist": "Appareil ajouté avec succès à la liste noire !",
|
||||||
|
"success_edit_blacklist": "Liste noire modifiée avec succès !",
|
||||||
|
"success_removed_blacklist": "Appareil supprimé de la liste noire !",
|
||||||
|
"uptimes_explanation": "Nombre d'appareils connectés en fonction de leur disponibilité"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Bûche",
|
"log": "Bûche",
|
||||||
@@ -320,23 +352,32 @@
|
|||||||
"add_success": "Entité créée avec succès !",
|
"add_success": "Entité créée avec succès !",
|
||||||
"assigned_inventory": "Inventaire assigné",
|
"assigned_inventory": "Inventaire assigné",
|
||||||
"cannot_delete": "Vous ne pouvez pas supprimer des entités qui ont des enfants. Supprimez les enfants de cette entité pour pouvoir la supprimer.",
|
"cannot_delete": "Vous ne pouvez pas supprimer des entités qui ont des enfants. Supprimez les enfants de cette entité pour pouvoir la supprimer.",
|
||||||
|
"confirm_map_delete": "Êtes-vous sûr de vouloir supprimer la carte {{name}}? Cette action ne peut pas être annulée",
|
||||||
"currently_selected_entity": "Entité actuellement sélectionnée : {{config}}",
|
"currently_selected_entity": "Entité actuellement sélectionnée : {{config}}",
|
||||||
"currently_selected_venue": "Lieu actuellement sélectionné : {{config}}",
|
"currently_selected_venue": "Lieu actuellement sélectionné : {{config}}",
|
||||||
"delete_success": "Entité supprimée avec succès",
|
"delete_success": "Entité supprimée avec succès",
|
||||||
"delete_warning": "Attention : cette opération ne peut pas être annulée",
|
"delete_warning": "Attention : cette opération ne peut pas être annulée",
|
||||||
|
"duplicate_from_node": "Dupliquer avec un nœud racine spécifique",
|
||||||
|
"duplicate_map": "Carte en double",
|
||||||
|
"duplicate_with_node": "Dupliquer {{mapName}} avec {{rootName}} comme nœud racine",
|
||||||
"edit_failure": "Échec de la mise à jour : {{error}}",
|
"edit_failure": "Échec de la mise à jour : {{error}}",
|
||||||
"enter_here": "Entrez les IP que vous souhaitez ajouter ici",
|
"enter_here": "Entrez les IP que vous souhaitez ajouter ici",
|
||||||
"entire_tree": "Site MAp",
|
"entire_tree": "Carte du réseau",
|
||||||
"entities": "Entités",
|
"entities": "Entités",
|
||||||
"entity": "Entité",
|
"entity": "Entité",
|
||||||
|
"error_deleting_map": "Erreur lors de la suppression de la carte : {{error}}",
|
||||||
"error_fetch_entity": "Erreur lors de la récupération des informations sur l'entité",
|
"error_fetch_entity": "Erreur lors de la récupération des informations sur l'entité",
|
||||||
"error_fetching": "Erreur lors de la récupération des entités",
|
"error_fetching": "Erreur lors de la récupération des entités",
|
||||||
"error_fetching_map": "Erreur lors de la récupération de la carte : {{error}}",
|
"error_fetching_map": "Erreur lors de la récupération de la carte : {{error}}",
|
||||||
|
"error_fetching_tree": "Erreur lors de la récupération de l'arborescence : {{error}}",
|
||||||
"error_saving": "Erreur lors de l'enregistrement de l'entité",
|
"error_saving": "Erreur lors de l'enregistrement de l'entité",
|
||||||
|
"error_saving_map": "Erreur lors de l'enregistrement de la carte : {{error}}",
|
||||||
"higher_priority": "Faire une priorité plus élevée",
|
"higher_priority": "Faire une priorité plus élevée",
|
||||||
"ip_detection": "Détection IP",
|
"ip_detection": "Détection IP",
|
||||||
"ip_formats": "Vous pouvez ajouter des adresses IPv4 ou IPv6 aux formats suivants :",
|
"ip_formats": "Vous pouvez ajouter des adresses IPv4 ou IPv6 aux formats suivants :",
|
||||||
"lower_priority": "Faire une priorité inférieure",
|
"lower_priority": "Faire une priorité inférieure",
|
||||||
|
"map": "Carte",
|
||||||
|
"map_delete_success": "Carte supprimée avec succès !",
|
||||||
"need_select_entity": "Vous devez sélectionner une entité dans le tableau ci-dessous",
|
"need_select_entity": "Vous devez sélectionner une entité dans le tableau ci-dessous",
|
||||||
"no_ips": "Aucune adresse IP sélectionnée",
|
"no_ips": "Aucune adresse IP sélectionnée",
|
||||||
"not_assigned": "Non attribué",
|
"not_assigned": "Non attribué",
|
||||||
@@ -344,6 +385,7 @@
|
|||||||
"select_entity": "Sélectionnez cette entité",
|
"select_entity": "Sélectionnez cette entité",
|
||||||
"selected_entity": "Entité sélectionnée",
|
"selected_entity": "Entité sélectionnée",
|
||||||
"selected_map": "Carte sélectionnée",
|
"selected_map": "Carte sélectionnée",
|
||||||
|
"tree_saved": "Carte enregistrée avec succès !",
|
||||||
"update_failure_error": "Erreur lors de la tentative de mise à jour de l'entité : {{error}}",
|
"update_failure_error": "Erreur lors de la tentative de mise à jour de l'entité : {{error}}",
|
||||||
"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)",
|
"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)",
|
||||||
"venues": "Les lieux"
|
"venues": "Les lieux"
|
||||||
@@ -541,6 +583,9 @@
|
|||||||
"verification_code": "Entrez votre vérification ici",
|
"verification_code": "Entrez votre vérification ici",
|
||||||
"wrong_code": "Le code de vérification saisi n'est pas valide."
|
"wrong_code": "Le code de vérification saisi n'est pas valide."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Provisioning"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "Quand souhaitez-vous redémarrer cet appareil ?",
|
"directions": "Quand souhaitez-vous redémarrer cet appareil ?",
|
||||||
"now": "Souhaitez-vous redémarrer cet appareil maintenant ?",
|
"now": "Souhaitez-vous redémarrer cet appareil maintenant ?",
|
||||||
@@ -551,7 +596,7 @@
|
|||||||
"channel": "Canal",
|
"channel": "Canal",
|
||||||
"directions": "Lancez une analyse wifi de cet appareil, ce qui devrait prendre environ 25 secondes.",
|
"directions": "Lancez une analyse wifi de cet appareil, ce qui devrait prendre environ 25 secondes.",
|
||||||
"re_scan": "Nouvelle analyse",
|
"re_scan": "Nouvelle analyse",
|
||||||
"result_directions": "Veuillez cliquer sur le bouton '$t(scan.re_scan)' si vous souhaitez effectuer un scan avec la même configuration que le précédent.",
|
"result_directions": "Vous pouvez cliquer sur le bouton 'Scan' en haut à droite pour $t(scan.re_scan)",
|
||||||
"results": "Résultats de l'analyse Wi-Fi",
|
"results": "Résultats de l'analyse Wi-Fi",
|
||||||
"scan": "Balayage",
|
"scan": "Balayage",
|
||||||
"scanning": "Balayage... ",
|
"scanning": "Balayage... ",
|
||||||
@@ -584,7 +629,7 @@
|
|||||||
"mac_prefix": "Préfixe MAC",
|
"mac_prefix": "Préfixe MAC",
|
||||||
"max_associations": "Max. Les associations",
|
"max_associations": "Max. Les associations",
|
||||||
"max_clients": "Max. Clients",
|
"max_clients": "Max. Clients",
|
||||||
"messages_transmitted": "Messages transmis",
|
"messages_transmitted": "Émission de messages",
|
||||||
"min_associations": "Min. Les associations",
|
"min_associations": "Min. Les associations",
|
||||||
"min_clients": "Min. Clients",
|
"min_clients": "Min. Clients",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
@@ -592,7 +637,7 @@
|
|||||||
"prefix_length": "Obligatoire, doit être d'une longueur de 6 caractères",
|
"prefix_length": "Obligatoire, doit être d'une longueur de 6 caractères",
|
||||||
"previous_runs": "Courses précédentes",
|
"previous_runs": "Courses précédentes",
|
||||||
"received": "reçu",
|
"received": "reçu",
|
||||||
"received_messages": "Messages reçus",
|
"received_messages": "Réception des messages",
|
||||||
"reconnect_interval": "Intervalle de reconnexion",
|
"reconnect_interval": "Intervalle de reconnexion",
|
||||||
"resume": "CV",
|
"resume": "CV",
|
||||||
"resume_success": "Exécution reprise !",
|
"resume_success": "Exécution reprise !",
|
||||||
@@ -615,11 +660,14 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"data": "Données (Ko)",
|
"data": "Données (Ko)",
|
||||||
|
"data_mb": "Données (Mo)",
|
||||||
"latest_statistics": "Dernières statistiques",
|
"latest_statistics": "Dernières statistiques",
|
||||||
"lifetime_stats": "Statistiques à vie",
|
"lifetime_stats": "Statistiques à vie",
|
||||||
|
"memory": "mémoire",
|
||||||
"no_interfaces": "Aucune statistique de durée de vie de l'interface disponible",
|
"no_interfaces": "Aucune statistique de durée de vie de l'interface disponible",
|
||||||
"show_latest": "Dernières statistiques",
|
"show_latest": "Dernières statistiques",
|
||||||
"title": "statistiques"
|
"title": "statistiques",
|
||||||
|
"used": "Mémoire utilisée %"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connection_status": "Statut",
|
"connection_status": "Statut",
|
||||||
@@ -631,9 +679,27 @@
|
|||||||
"percentage_free": "{{percentage}}% de {{total}} gratuit",
|
"percentage_free": "{{percentage}}% de {{total}} gratuit",
|
||||||
"percentage_used": "{{percentage}}% de {{total}} utilisé",
|
"percentage_used": "{{percentage}}% de {{total}} utilisé",
|
||||||
"title": "#{{serialNumber}} état",
|
"title": "#{{serialNumber}} état",
|
||||||
|
"total_memory": "Mémoire totale",
|
||||||
"uptime": "La disponibilité",
|
"uptime": "La disponibilité",
|
||||||
"used_total_memory": "{{used}} utilisé / {{total}} total"
|
"used_total_memory": "{{used}} utilisé / {{total}} total"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"add_device_subscriber_explanation": "Pour réclamer d'autres appareils, vous pouvez utiliser notre barre de recherche ou réclamer directement à partir du tableau. Si un appareil a déjà été réclamé par un utilisateur, vous devrez accéder à ses détails et le désaffecter avant de le réclamer.",
|
||||||
|
"create": "Créer un abonné",
|
||||||
|
"devices_one": "{{count}} Appareil",
|
||||||
|
"devices_other": "{{count}} appareils",
|
||||||
|
"edit": "Modifier l'abonné",
|
||||||
|
"error_create": "Erreur lors de la création de l'abonné : {{error}}",
|
||||||
|
"error_delete": "Erreur lors de la suppression de l'abonné : {{error}}",
|
||||||
|
"error_fetching": "Erreur lors de la récupération des abonnés : {{error}}",
|
||||||
|
"error_fetching_single": "Erreur lors de la récupération de l'abonné : {{error}}",
|
||||||
|
"error_update": "Erreur lors de la mise à jour de l'abonné : {{error}}",
|
||||||
|
"is_already_claimed": "est déjà réclamé par",
|
||||||
|
"subscribers": "Les abonnés",
|
||||||
|
"success_create": "Abonné créé avec succès !",
|
||||||
|
"success_delete": "Abonné supprimé avec succès !",
|
||||||
|
"success_update": "Abonné mis à jour avec succès !"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Erreur lors de la récupération des informations système",
|
"error_fetching": "Erreur lors de la récupération des informations système",
|
||||||
"error_reloading": "Erreur lors du rechargement : {{error}}",
|
"error_reloading": "Erreur lors du rechargement : {{error}}",
|
||||||
@@ -723,6 +789,7 @@
|
|||||||
"send_code_again": "Envoyer code à nouveau",
|
"send_code_again": "Envoyer code à nouveau",
|
||||||
"show_hide_password": "Afficher/Masquer le mot de passe",
|
"show_hide_password": "Afficher/Masquer le mot de passe",
|
||||||
"successful_validation": "Numéro de téléphone validé ! Cliquez sur le bouton Enregistrer pour le lier à votre profil",
|
"successful_validation": "Numéro de téléphone validé ! Cliquez sur le bouton Enregistrer pour le lier à votre profil",
|
||||||
|
"table_title": "Utilisateurs administrateurs",
|
||||||
"update_failure": "Erreur lors de la tentative de mise à jour : {{error}}",
|
"update_failure": "Erreur lors de la tentative de mise à jour : {{error}}",
|
||||||
"update_failure_title": "mise à jour a échoué",
|
"update_failure_title": "mise à jour a échoué",
|
||||||
"update_success": "L'utilisateur a bien été mis à jour",
|
"update_success": "L'utilisateur a bien été mis à jour",
|
||||||
@@ -738,7 +805,10 @@
|
|||||||
"associations": "Les associations",
|
"associations": "Les associations",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
"network_diagram": "Diagramme de réseau",
|
"network_diagram": "Diagramme de réseau",
|
||||||
|
"override_dfs": "Remplacer DFS",
|
||||||
"radios": "Radios",
|
"radios": "Radios",
|
||||||
"title": "Analyse Wi-Fi"
|
"scan_warning": "Votre radio 5G est sur un canal radar, vous devez activer \"Override DFS\" pour permettre le balayage de tous les canaux 5G",
|
||||||
|
"title": "Analyse Wi-Fi",
|
||||||
|
"vendor": "vendeur"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"blink": "Piscar",
|
"blink": "Piscar",
|
||||||
"device_leds": "LEDs do dispositivo",
|
"device_leds": "LEDs do dispositivo",
|
||||||
"execute_now": "Você gostaria de definir este padrão agora?",
|
"execute_now": "Você gostaria de definir este padrão agora?",
|
||||||
|
"explanation": "Que padrão você gostaria de definir neste dispositivo por 30 segundos?",
|
||||||
"pattern": "Escolha o padrão que deseja usar:",
|
"pattern": "Escolha o padrão que deseja usar:",
|
||||||
"set_leds": "Definir LEDs",
|
"set_leds": "Definir LEDs",
|
||||||
"when_blink_leds": "Quando você gostaria de fazer os LEDs do dispositivo piscarem?"
|
"when_blink_leds": "Quando você gostaria de fazer os LEDs do dispositivo piscarem?"
|
||||||
@@ -37,13 +38,16 @@
|
|||||||
"add_note": "Adicionar nota",
|
"add_note": "Adicionar nota",
|
||||||
"add_note_explanation": "Escreva sua nova nota abaixo e clique no botão '+' quando terminar",
|
"add_note_explanation": "Escreva sua nova nota abaixo e clique no botão '+' quando terminar",
|
||||||
"adding_ellipsis": "Adicionando ...",
|
"adding_ellipsis": "Adicionando ...",
|
||||||
|
"all": "Todos",
|
||||||
"are_you_sure": "Você tem certeza?",
|
"are_you_sure": "Você tem certeza?",
|
||||||
"back_to_login": "Volte ao login",
|
"back_to_login": "Volte ao login",
|
||||||
"back_to_start": "Voltar ao Início",
|
"back_to_start": "Voltar ao Início",
|
||||||
|
"blacklist": "Lista negra",
|
||||||
"by": "Por",
|
"by": "Por",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"certificate": "Certificado",
|
"certificate": "Certificado",
|
||||||
"certificates": "Certificados",
|
"certificates": "Certificados",
|
||||||
|
"claim": "Afirmação",
|
||||||
"clear": "Claro",
|
"clear": "Claro",
|
||||||
"close": "Perto",
|
"close": "Perto",
|
||||||
"code": "Código",
|
"code": "Código",
|
||||||
@@ -58,16 +62,19 @@
|
|||||||
"confirm_stop_editing": "Tem certeza que deseja parar de editar? Isso cancelará todas as alterações não salvas que você fez.",
|
"confirm_stop_editing": "Tem certeza que deseja parar de editar? Isso cancelará todas as alterações não salvas que você fez.",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
"copied": "Copiado!",
|
"copied": "Copiado!",
|
||||||
|
"copied_to_clipboard": "Copiado para a área de transferência!",
|
||||||
"copy_to_clipboard": "Copiar para área de transferência",
|
"copy_to_clipboard": "Copiar para área de transferência",
|
||||||
"create": "Crio",
|
"create": "Crio",
|
||||||
"created": "Criado",
|
"created": "Criado",
|
||||||
"created_by": "Criado Por",
|
"created_by": "Criado Por",
|
||||||
|
"creator": "O Criador",
|
||||||
"current": "Atual",
|
"current": "Atual",
|
||||||
"custom_date": "Data personalizada",
|
"custom_date": "Data personalizada",
|
||||||
"dashboard": "painel de controle",
|
"dashboard": "painel de controle",
|
||||||
"date": "Encontro",
|
"date": "Encontro",
|
||||||
"day": "dia",
|
"day": "dia",
|
||||||
"days": "dias",
|
"days": "dias",
|
||||||
|
"default_map": "Mapa Padrão",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"delete_device": "Apagar dispositivo",
|
"delete_device": "Apagar dispositivo",
|
||||||
"details": "Detalhes",
|
"details": "Detalhes",
|
||||||
@@ -85,6 +92,7 @@
|
|||||||
"dismiss": "Dispensar",
|
"dismiss": "Dispensar",
|
||||||
"do_now": "Faça agora!",
|
"do_now": "Faça agora!",
|
||||||
"download": "Baixar",
|
"download": "Baixar",
|
||||||
|
"duplicate": "Duplicado",
|
||||||
"duration": "Duração",
|
"duration": "Duração",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"edit_user": "Editar",
|
"edit_user": "Editar",
|
||||||
@@ -94,6 +102,7 @@
|
|||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"error_adding_note": "Erro ao adicionar nota",
|
"error_adding_note": "Erro ao adicionar nota",
|
||||||
"error_code": "Erro de código",
|
"error_code": "Erro de código",
|
||||||
|
"errors": "Erros",
|
||||||
"execute_now": "Você gostaria de executar este comando agora?",
|
"execute_now": "Você gostaria de executar este comando agora?",
|
||||||
"executed": "Executado",
|
"executed": "Executado",
|
||||||
"exit": "Saída",
|
"exit": "Saída",
|
||||||
@@ -110,6 +119,7 @@
|
|||||||
"hours": "horas",
|
"hours": "horas",
|
||||||
"id": "identidade",
|
"id": "identidade",
|
||||||
"invalid_credentials": "Nome de usuário e / ou senha inválidos",
|
"invalid_credentials": "Nome de usuário e / ou senha inválidos",
|
||||||
|
"invalid_date_explanation": "Data inválida, use o calendário acessível com o botão à direita",
|
||||||
"invalid_file": "O arquivo escolhido era inválido, por favor, leia as instruções e ajuste seu arquivo de acordo",
|
"invalid_file": "O arquivo escolhido era inválido, por favor, leia as instruções e ajuste seu arquivo de acordo",
|
||||||
"invalid_password": "Esta senha não está de acordo com as regras básicas de senha. Visite nossa página de Política de Senha para saber mais",
|
"invalid_password": "Esta senha não está de acordo com as regras básicas de senha. Visite nossa página de Política de Senha para saber mais",
|
||||||
"invalid_pem": "Seu arquivo .pem é inválido. Deve começar com '----- BEGIN CERTIFICATE -----' OU '----- BEGIN PRIVATE KEY -----' e deve terminar com '----- END CERTIFICATE --- - 'OU' ----- END PRIVATE KEY ----- '",
|
"invalid_pem": "Seu arquivo .pem é inválido. Deve começar com '----- BEGIN CERTIFICATE -----' OU '----- BEGIN PRIVATE KEY -----' e deve terminar com '----- END CERTIFICATE --- - 'OU' ----- END PRIVATE KEY ----- '",
|
||||||
@@ -142,17 +152,20 @@
|
|||||||
"no_items": "Nenhum item",
|
"no_items": "Nenhum item",
|
||||||
"none": "Nenhum",
|
"none": "Nenhum",
|
||||||
"not_connected": "Não conectado",
|
"not_connected": "Não conectado",
|
||||||
"of_connected": "% de dispositivos",
|
"of_connected": "% de dispositivos conectados",
|
||||||
"off": "Fora",
|
"off": "Fora",
|
||||||
"on": "em",
|
"on": "em",
|
||||||
"optional": "Opcional",
|
"optional": "Opcional",
|
||||||
"overall_health": "Saúde geral",
|
"overall_health": "Saúde geral",
|
||||||
"password_policy": "Política de Senha",
|
"password_policy": "Política de Senha",
|
||||||
|
"preferences": "Preferências",
|
||||||
"preview": "Visualizar",
|
"preview": "Visualizar",
|
||||||
"program": "Programa",
|
"program": "Programa",
|
||||||
|
"reason": "RAZÃO",
|
||||||
"recorded": "Gravado",
|
"recorded": "Gravado",
|
||||||
"refresh": "REFRESH",
|
"refresh": "REFRESH",
|
||||||
"refresh_device": "Atualizar dispositivo",
|
"refresh_device": "Atualizar dispositivo",
|
||||||
|
"remove_claim": "Remover reivindicação",
|
||||||
"required": "Requeridos",
|
"required": "Requeridos",
|
||||||
"result": "Resultado",
|
"result": "Resultado",
|
||||||
"save": "Salve",
|
"save": "Salve",
|
||||||
@@ -170,12 +183,14 @@
|
|||||||
"show_all": "mostre tudo",
|
"show_all": "mostre tudo",
|
||||||
"socket_connection_closed": "Conexão fechada!",
|
"socket_connection_closed": "Conexão fechada!",
|
||||||
"start": "Começar",
|
"start": "Começar",
|
||||||
|
"status": "Status",
|
||||||
"stop_editing": "Pare de editar",
|
"stop_editing": "Pare de editar",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"submitted": "Submetido",
|
"submitted": "Submetido",
|
||||||
"success": "Sucesso",
|
"success": "Sucesso",
|
||||||
"system": "Sistema",
|
"system": "Sistema",
|
||||||
"table": "Mesa",
|
"table": "Mesa",
|
||||||
|
"time_per_device": "Dispositivo / segundo",
|
||||||
"timestamp": "tempo",
|
"timestamp": "tempo",
|
||||||
"to": "Para",
|
"to": "Para",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
@@ -190,6 +205,8 @@
|
|||||||
"uuid": "UUID",
|
"uuid": "UUID",
|
||||||
"vendors": "Vendedores",
|
"vendors": "Vendedores",
|
||||||
"view_more": "Veja mais",
|
"view_more": "Veja mais",
|
||||||
|
"visibility": "visibilidade",
|
||||||
|
"waiting_for_update": "Aguardando atualização",
|
||||||
"yes": "sim"
|
"yes": "sim"
|
||||||
},
|
},
|
||||||
"configuration": {
|
"configuration": {
|
||||||
@@ -210,6 +227,8 @@
|
|||||||
"creation_success": "Configuração criada com sucesso!",
|
"creation_success": "Configuração criada com sucesso!",
|
||||||
"currently_associated": "Configuração atual associada: {{config}}",
|
"currently_associated": "Configuração atual associada: {{config}}",
|
||||||
"currently_selected_config": "Configuração atualmente selecionada: {{config}}",
|
"currently_selected_config": "Configuração atualmente selecionada: {{config}}",
|
||||||
|
"default_configs": "Configurações padrão",
|
||||||
|
"default_configurations": "Configurações padrão",
|
||||||
"delete_config": "Excluir configuração",
|
"delete_config": "Excluir configuração",
|
||||||
"details": "Detalhes",
|
"details": "Detalhes",
|
||||||
"device_password": "Senha",
|
"device_password": "Senha",
|
||||||
@@ -218,6 +237,7 @@
|
|||||||
"devices_affected": "Dispositivos afetados por esta configuração:",
|
"devices_affected": "Dispositivos afetados por esta configuração:",
|
||||||
"edit_configuration": "Editar configuração",
|
"edit_configuration": "Editar configuração",
|
||||||
"error_delete": "Erro ao tentar excluir: {{error}}",
|
"error_delete": "Erro ao tentar excluir: {{error}}",
|
||||||
|
"error_delete_blacklist": "Erro ao excluir da lista negra: {{error}}",
|
||||||
"error_fetching_config": "Erro ao buscar configuração",
|
"error_fetching_config": "Erro ao buscar configuração",
|
||||||
"error_trying_delete": "Erro ao tentar excluir: {{error}}",
|
"error_trying_delete": "Erro ao tentar excluir: {{error}}",
|
||||||
"error_update": "Erro: {{error}}",
|
"error_update": "Erro: {{error}}",
|
||||||
@@ -261,6 +281,7 @@
|
|||||||
"contact": {
|
"contact": {
|
||||||
"access_pin": "PIN de acesso",
|
"access_pin": "PIN de acesso",
|
||||||
"add_contact": "Adicionar contato",
|
"add_contact": "Adicionar contato",
|
||||||
|
"contact": "Contato",
|
||||||
"create_contact": "Criar Contato",
|
"create_contact": "Criar Contato",
|
||||||
"currently_selected_contact": "Contato atualmente selecionado: {{contact}}",
|
"currently_selected_contact": "Contato atualmente selecionado: {{contact}}",
|
||||||
"delete": "Excluir contato?",
|
"delete": "Excluir contato?",
|
||||||
@@ -300,12 +321,23 @@
|
|||||||
"healthchecks_title": "Excluir verificações de saúde"
|
"healthchecks_title": "Excluir verificações de saúde"
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
"add_to_blacklist": "Adicionar dispositivo à lista negra",
|
||||||
|
"all_devices": "Todos os dispositivos",
|
||||||
|
"blacklisted_on": "Encontro",
|
||||||
|
"capabilities": "Recursos",
|
||||||
"certificate_explanation": "Certificados de dispositivos conectados",
|
"certificate_explanation": "Certificados de dispositivos conectados",
|
||||||
|
"edit_blacklist": "Editar dispositivo na lista negra",
|
||||||
|
"error_adding_blacklist": "Erro ao adicionar dispositivo à lista negra: {{error}}",
|
||||||
|
"error_edit_blacklist": "Erro ao editar a lista negra: {{error}}",
|
||||||
"error_fetching_device": "Erro ao buscar informações do dispositivo: {{error}}",
|
"error_fetching_device": "Erro ao buscar informações do dispositivo: {{error}}",
|
||||||
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}",
|
"error_fetching_devices": "Erro ao buscar dispositivos: {{error}}",
|
||||||
"health_explanation": "Saúde de dispositivos conectados",
|
"health_explanation": "Integridade dos dispositivos conectados ((Dispositivos = 100% * 100 + Dispositivos> 90% * 95 + Dispositivos> 60% * 75 + Dispositivos <60% * 35) / Dispositivos Conectados)",
|
||||||
"memory_explanation": "Memória usada por dispositivos conectados",
|
"memory_explanation": "Quantidade de dispositivos conectados com a memória correspondente usada%",
|
||||||
"uptimes_explanation": "Há tempo em que os dispositivos conectados estão ativados e conectados"
|
"remove_from_blacklist": "Remover da lista negra",
|
||||||
|
"success_added_blacklist": "Dispositivo adicionado à lista negra com sucesso!",
|
||||||
|
"success_edit_blacklist": "Lista negra editada com sucesso!",
|
||||||
|
"success_removed_blacklist": "Dispositivo removido com sucesso da lista negra!",
|
||||||
|
"uptimes_explanation": "Quantidade de dispositivos conectados com base em seu tempo de atividade"
|
||||||
},
|
},
|
||||||
"device_logs": {
|
"device_logs": {
|
||||||
"log": "Registro",
|
"log": "Registro",
|
||||||
@@ -320,23 +352,32 @@
|
|||||||
"add_success": "Entidade criada com sucesso!",
|
"add_success": "Entidade criada com sucesso!",
|
||||||
"assigned_inventory": "Estoque Atribuído",
|
"assigned_inventory": "Estoque Atribuído",
|
||||||
"cannot_delete": "Você não pode excluir entidades que têm filhos. Exclua os filhos desta entidade para poder excluí-la.",
|
"cannot_delete": "Você não pode excluir entidades que têm filhos. Exclua os filhos desta entidade para poder excluí-la.",
|
||||||
|
"confirm_map_delete": "Tem certeza que deseja excluir o mapa {{name}}? Esta ação não pode ser revertida",
|
||||||
"currently_selected_entity": "Entidade atualmente selecionada: {{config}}",
|
"currently_selected_entity": "Entidade atualmente selecionada: {{config}}",
|
||||||
"currently_selected_venue": "Local selecionado atualmente: {{config}}",
|
"currently_selected_venue": "Local selecionado atualmente: {{config}}",
|
||||||
"delete_success": "Entidade excluída com sucesso",
|
"delete_success": "Entidade excluída com sucesso",
|
||||||
"delete_warning": "Aviso: esta operação não pode ser revertida",
|
"delete_warning": "Aviso: esta operação não pode ser revertida",
|
||||||
|
"duplicate_from_node": "Duplicar com nó raiz específico",
|
||||||
|
"duplicate_map": "Mapa duplicado",
|
||||||
|
"duplicate_with_node": "Duplicar {{mapName}} com {{rootName}} como nó raiz",
|
||||||
"edit_failure": "Atualização malsucedida: {{error}}",
|
"edit_failure": "Atualização malsucedida: {{error}}",
|
||||||
"enter_here": "Digite o (s) IP (s) que deseja adicionar aqui",
|
"enter_here": "Digite o (s) IP (s) que deseja adicionar aqui",
|
||||||
"entire_tree": "Mapa do Site",
|
"entire_tree": "Mapa de Rede",
|
||||||
"entities": "Entidades",
|
"entities": "Entidades",
|
||||||
"entity": "Entidade",
|
"entity": "Entidade",
|
||||||
|
"error_deleting_map": "Erro ao excluir mapa: {{error}}",
|
||||||
"error_fetch_entity": "Erro ao buscar informações da entidade",
|
"error_fetch_entity": "Erro ao buscar informações da entidade",
|
||||||
"error_fetching": "Erro ao buscar entidades",
|
"error_fetching": "Erro ao buscar entidades",
|
||||||
"error_fetching_map": "Erro ao buscar mapa: {{error}}",
|
"error_fetching_map": "Erro ao buscar mapa: {{error}}",
|
||||||
|
"error_fetching_tree": "Erro ao buscar árvore: {{error}}",
|
||||||
"error_saving": "Erro ao salvar entidade",
|
"error_saving": "Erro ao salvar entidade",
|
||||||
|
"error_saving_map": "Erro ao salvar o mapa: {{error}}",
|
||||||
"higher_priority": "Dê maior prioridade",
|
"higher_priority": "Dê maior prioridade",
|
||||||
"ip_detection": "Detecção de IP",
|
"ip_detection": "Detecção de IP",
|
||||||
"ip_formats": "Você pode adicionar endereços IPv4 ou IPv6 nos seguintes formatos:",
|
"ip_formats": "Você pode adicionar endereços IPv4 ou IPv6 nos seguintes formatos:",
|
||||||
"lower_priority": "Faça menor prioridade",
|
"lower_priority": "Faça menor prioridade",
|
||||||
|
"map": "Mapa",
|
||||||
|
"map_delete_success": "Mapa excluído com sucesso!",
|
||||||
"need_select_entity": "Você precisa selecionar uma entidade da tabela abaixo",
|
"need_select_entity": "Você precisa selecionar uma entidade da tabela abaixo",
|
||||||
"no_ips": "Nenhum IP selecionado",
|
"no_ips": "Nenhum IP selecionado",
|
||||||
"not_assigned": "Não atribuído",
|
"not_assigned": "Não atribuído",
|
||||||
@@ -344,6 +385,7 @@
|
|||||||
"select_entity": "Selecione esta Entidade",
|
"select_entity": "Selecione esta Entidade",
|
||||||
"selected_entity": "Entidade Selecionada",
|
"selected_entity": "Entidade Selecionada",
|
||||||
"selected_map": "Mapa Selecionado",
|
"selected_map": "Mapa Selecionado",
|
||||||
|
"tree_saved": "Mapa salvo com sucesso!",
|
||||||
"update_failure_error": "Erro ao tentar atualizar a entidade: {{error}}",
|
"update_failure_error": "Erro ao tentar atualizar a entidade: {{error}}",
|
||||||
"valid_serial": "Precisa ser um número de série válido (12 caracteres HEX)",
|
"valid_serial": "Precisa ser um número de série válido (12 caracteres HEX)",
|
||||||
"venues": "Locais"
|
"venues": "Locais"
|
||||||
@@ -541,6 +583,9 @@
|
|||||||
"verification_code": "Insira sua verificação aqui",
|
"verification_code": "Insira sua verificação aqui",
|
||||||
"wrong_code": "O código de verificação inserido não é válido."
|
"wrong_code": "O código de verificação inserido não é válido."
|
||||||
},
|
},
|
||||||
|
"preferences": {
|
||||||
|
"provisioning": "Provisioning"
|
||||||
|
},
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"directions": "Quando você gostaria de reinicializar este dispositivo?",
|
"directions": "Quando você gostaria de reinicializar este dispositivo?",
|
||||||
"now": "Você gostaria de reiniciar este dispositivo agora?",
|
"now": "Você gostaria de reiniciar este dispositivo agora?",
|
||||||
@@ -551,7 +596,7 @@
|
|||||||
"channel": "Canal",
|
"channel": "Canal",
|
||||||
"directions": "Inicie uma verificação de wi-fi deste dispositivo, o que deve levar aproximadamente 25 segundos.",
|
"directions": "Inicie uma verificação de wi-fi deste dispositivo, o que deve levar aproximadamente 25 segundos.",
|
||||||
"re_scan": "Verificar novamente",
|
"re_scan": "Verificar novamente",
|
||||||
"result_directions": "Clique no botão '$ t (scan.re_scan)' se desejar fazer uma varredura com a mesma configuração da anterior.",
|
"result_directions": "Você pode clicar no botão 'Scan' no canto superior direito para $t(scan.re_scan)",
|
||||||
"results": "Resultados da verificação de Wi-Fi",
|
"results": "Resultados da verificação de Wi-Fi",
|
||||||
"scan": "Varredura",
|
"scan": "Varredura",
|
||||||
"scanning": "Scanning... ",
|
"scanning": "Scanning... ",
|
||||||
@@ -584,7 +629,7 @@
|
|||||||
"mac_prefix": "Prefixo MAC",
|
"mac_prefix": "Prefixo MAC",
|
||||||
"max_associations": "Máx. Associações",
|
"max_associations": "Máx. Associações",
|
||||||
"max_clients": "Máx. Clientes",
|
"max_clients": "Máx. Clientes",
|
||||||
"messages_transmitted": "Mensagens Transmitidas",
|
"messages_transmitted": "Msgs TX",
|
||||||
"min_associations": "Min. Associações",
|
"min_associations": "Min. Associações",
|
||||||
"min_clients": "Min. Clientes",
|
"min_clients": "Min. Clientes",
|
||||||
"pause": "pausa",
|
"pause": "pausa",
|
||||||
@@ -592,7 +637,7 @@
|
|||||||
"prefix_length": "Obrigatório, deve ter 6 caracteres",
|
"prefix_length": "Obrigatório, deve ter 6 caracteres",
|
||||||
"previous_runs": "Execuções anteriores",
|
"previous_runs": "Execuções anteriores",
|
||||||
"received": "recebido",
|
"received": "recebido",
|
||||||
"received_messages": "Mensagens recebidas",
|
"received_messages": "Msgs RX",
|
||||||
"reconnect_interval": "Intervalo de reconexão",
|
"reconnect_interval": "Intervalo de reconexão",
|
||||||
"resume": "Currículo",
|
"resume": "Currículo",
|
||||||
"resume_success": "Executar retomado!",
|
"resume_success": "Executar retomado!",
|
||||||
@@ -615,11 +660,14 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"data": "Dados (KB)",
|
"data": "Dados (KB)",
|
||||||
|
"data_mb": "Dados (MB)",
|
||||||
"latest_statistics": "Estatísticas mais recentes",
|
"latest_statistics": "Estatísticas mais recentes",
|
||||||
"lifetime_stats": "Estatísticas de vida",
|
"lifetime_stats": "Estatísticas de vida",
|
||||||
|
"memory": "Memória",
|
||||||
"no_interfaces": "Nenhuma estatística de tempo de vida da interface disponível",
|
"no_interfaces": "Nenhuma estatística de tempo de vida da interface disponível",
|
||||||
"show_latest": "Últimas estatísticas",
|
"show_latest": "Últimas estatísticas",
|
||||||
"title": "Estatisticas"
|
"title": "Estatisticas",
|
||||||
|
"used": "Memoria usada %"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"connection_status": "Status",
|
"connection_status": "Status",
|
||||||
@@ -631,9 +679,27 @@
|
|||||||
"percentage_free": "{{percentage}}% de {{total}} grátis",
|
"percentage_free": "{{percentage}}% de {{total}} grátis",
|
||||||
"percentage_used": "{{percentage}}% de {{total}} usado",
|
"percentage_used": "{{percentage}}% de {{total}} usado",
|
||||||
"title": "#{{serialNumber}} status",
|
"title": "#{{serialNumber}} status",
|
||||||
|
"total_memory": "Memória total",
|
||||||
"uptime": "Tempo de atividade",
|
"uptime": "Tempo de atividade",
|
||||||
"used_total_memory": "{{used}} usado / {{total}} total"
|
"used_total_memory": "{{used}} usado / {{total}} total"
|
||||||
},
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"add_device_subscriber_explanation": "Para reivindicar outros dispositivos, você pode usar nossa barra de pesquisa ou reivindicar diretamente na tabela. Se um dispositivo já foi reivindicado por um usuário, você precisará acessar os detalhes dele e cancelar a atribuição antes de reivindicá-lo.",
|
||||||
|
"create": "Criar assinante",
|
||||||
|
"devices_one": "{{count}} Dispositivo",
|
||||||
|
"devices_other": "{{count}} dispositivos",
|
||||||
|
"edit": "Editar Assinante",
|
||||||
|
"error_create": "Erro ao criar assinante: {{error}}",
|
||||||
|
"error_delete": "Erro ao excluir assinante: {{error}}",
|
||||||
|
"error_fetching": "Erro ao buscar assinantes: {{error}}",
|
||||||
|
"error_fetching_single": "Erro ao buscar assinante: {{error}}",
|
||||||
|
"error_update": "Erro ao atualizar assinante: {{error}}",
|
||||||
|
"is_already_claimed": "já é reivindicado por",
|
||||||
|
"subscribers": "Inscritos",
|
||||||
|
"success_create": "Assinante criado com sucesso!",
|
||||||
|
"success_delete": "Assinante excluído com sucesso!",
|
||||||
|
"success_update": "Assinante atualizado com sucesso!"
|
||||||
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"error_fetching": "Erro ao buscar informações do sistema",
|
"error_fetching": "Erro ao buscar informações do sistema",
|
||||||
"error_reloading": "Erro ao recarregar: {{error}}",
|
"error_reloading": "Erro ao recarregar: {{error}}",
|
||||||
@@ -723,6 +789,7 @@
|
|||||||
"send_code_again": "Envie o Código Novamente",
|
"send_code_again": "Envie o Código Novamente",
|
||||||
"show_hide_password": "Mostrar / ocultar senha",
|
"show_hide_password": "Mostrar / ocultar senha",
|
||||||
"successful_validation": "Número de telefone validado! Clique no botão Salvar para vinculá-lo ao seu perfil",
|
"successful_validation": "Número de telefone validado! Clique no botão Salvar para vinculá-lo ao seu perfil",
|
||||||
|
"table_title": "Usuários administrativos",
|
||||||
"update_failure": "Erro ao tentar atualizar: {{error}}",
|
"update_failure": "Erro ao tentar atualizar: {{error}}",
|
||||||
"update_failure_title": "Atualização falhou",
|
"update_failure_title": "Atualização falhou",
|
||||||
"update_success": "Usuário atualizado com sucesso",
|
"update_success": "Usuário atualizado com sucesso",
|
||||||
@@ -738,7 +805,10 @@
|
|||||||
"associations": "Associações",
|
"associations": "Associações",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
"network_diagram": "Diagrama de rede",
|
"network_diagram": "Diagrama de rede",
|
||||||
|
"override_dfs": "Substituir DFS",
|
||||||
"radios": "Rádios",
|
"radios": "Rádios",
|
||||||
"title": "Análise de Wi-Fi"
|
"scan_warning": "Seu rádio 5G está em um canal de radar, você deve habilitar “Override DFS” para permitir a varredura de todos os canais 5G",
|
||||||
|
"title": "Análise de Wi-Fi",
|
||||||
|
"vendor": "fornecedor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/NotFound.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/devices/cig_wf160d.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
src/assets/devices/cig_wf188.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/devices/cig_wf188n.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/devices/cig_wf194c.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/assets/devices/cig_wf194c4.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/assets/devices/cig_wf808.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
src/assets/devices/cig_wf809.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
src/assets/devices/edgecore_eap101.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
src/assets/devices/edgecore_eap102.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
src/assets/devices/edgecore_ecs4100-12ph.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/devices/edgecore_ecw5211.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
src/assets/devices/edgecore_ecw5410.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
src/assets/devices/edgecore_oap100.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/assets/devices/edgecore_spw2ac1200-lan-poe.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/devices/edgecore_spw2ac1200.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/devices/edgecore_ssw2ac2600.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
src/assets/devices/hfcl_ion4.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/devices/hfcl_ion4.yml.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/devices/indio_um-305ac.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src/assets/devices/linksys_e8450-ubi.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
src/assets/devices/linksys_ea6350-v4.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/assets/devices/linksys_ea6350.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/assets/devices/linksys_ea8300.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
src/assets/devices/tp-link_ec420-g1.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
src/assets/devices/tplink_ec420.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
src/assets/devices/tplink_ex227.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/devices/tplink_ex228.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/devices/tplink_ex447.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
src/assets/devices/wallys_dr40x9.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
src/assets/devices/wallys_dr6018.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src/assets/devices/wallys_dr6018_v4.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
@@ -13,6 +13,7 @@ import {
|
|||||||
cilArrowTop,
|
cilArrowTop,
|
||||||
cilAsterisk,
|
cilAsterisk,
|
||||||
cilBan,
|
cilBan,
|
||||||
|
cilBarcode,
|
||||||
cilBasket,
|
cilBasket,
|
||||||
cilBell,
|
cilBell,
|
||||||
cilBold,
|
cilBold,
|
||||||
@@ -108,6 +109,7 @@ export const icons = {
|
|||||||
cilArrowTop,
|
cilArrowTop,
|
||||||
cilAsterisk,
|
cilAsterisk,
|
||||||
cilBan,
|
cilBan,
|
||||||
|
cilBarcode,
|
||||||
cilBasket,
|
cilBasket,
|
||||||
cilBell,
|
cilBell,
|
||||||
cilBold,
|
cilBold,
|
||||||
|
|||||||
163
src/components/AddConfigurationModal/Form.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import {
|
||||||
|
CForm,
|
||||||
|
CInput,
|
||||||
|
CLabel,
|
||||||
|
CCol,
|
||||||
|
CFormGroup,
|
||||||
|
CInvalidFeedback,
|
||||||
|
CFormText,
|
||||||
|
CRow,
|
||||||
|
CTextarea,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CopyToClipboardButton } from 'ucentral-libs';
|
||||||
|
|
||||||
|
const AddDefaultConfigurationForm = ({
|
||||||
|
t,
|
||||||
|
disable,
|
||||||
|
fields,
|
||||||
|
updateField,
|
||||||
|
updateFieldWithKey,
|
||||||
|
deviceTypes,
|
||||||
|
}) => {
|
||||||
|
const [typeOptions, setTypeOptions] = useState([]);
|
||||||
|
const [chosenTypes, setChosenTypes] = useState([]);
|
||||||
|
|
||||||
|
const parseOptions = () => {
|
||||||
|
const options = [{ value: '*', label: 'All' }];
|
||||||
|
const newOptions = deviceTypes.map((option) => ({
|
||||||
|
value: option,
|
||||||
|
label: option,
|
||||||
|
}));
|
||||||
|
options.push(...newOptions);
|
||||||
|
setTypeOptions(options);
|
||||||
|
setChosenTypes([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeOnChange = (chosenArray) => {
|
||||||
|
const allIndex = chosenArray.findIndex((el) => el.value === '*');
|
||||||
|
|
||||||
|
// If the All option was chosen before, we take it out of the array
|
||||||
|
if (allIndex === 0 && chosenTypes.length > 0) {
|
||||||
|
const newResults = chosenArray.slice(1);
|
||||||
|
setChosenTypes(newResults);
|
||||||
|
updateFieldWithKey('deviceTypes', {
|
||||||
|
value: newResults.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else if (allIndex > 0) {
|
||||||
|
setChosenTypes([{ value: '*', label: 'All' }]);
|
||||||
|
updateFieldWithKey('deviceTypes', { value: ['*'], error: false, notEmpty: true });
|
||||||
|
} else if (chosenArray.length > 0) {
|
||||||
|
setChosenTypes(chosenArray);
|
||||||
|
updateFieldWithKey('deviceTypes', {
|
||||||
|
value: chosenArray.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setChosenTypes([]);
|
||||||
|
updateFieldWithKey('deviceTypes', { value: [], error: false, notEmpty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseOptions();
|
||||||
|
}, [deviceTypes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CForm>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="name">
|
||||||
|
{t('user.name')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<CInput
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.name.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.name.error}
|
||||||
|
disabled={disable}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="description">
|
||||||
|
{t('user.description')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.description.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.description.error}
|
||||||
|
disabled={disable}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CLabel col htmlFor="deviceTypes">
|
||||||
|
<div>{t('configuration.supported_device_types')}:</div>
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
id="deviceTypes"
|
||||||
|
options={typeOptions}
|
||||||
|
onChange={typeOnChange}
|
||||||
|
value={chosenTypes}
|
||||||
|
className={`basic-multi-select ${fields.deviceTypes.error ? 'border-danger' : ''}`}
|
||||||
|
classNamePrefix="select"
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.deviceTypes.error} color="danger">
|
||||||
|
{t('configuration.need_device_type')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<div className="pb-3">
|
||||||
|
{t('configure.enter_new')}
|
||||||
|
<CopyToClipboardButton t={t} size="sm" content={fields.configuration.value} />
|
||||||
|
</div>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CCol>
|
||||||
|
<CTextarea
|
||||||
|
style={{ overflowY: 'scroll', height: '500px' }}
|
||||||
|
id="configuration"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.configuration.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.configuration.error}
|
||||||
|
disabled={disable}
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.configuration.error} color="danger">
|
||||||
|
{t('configure.valid_json')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddDefaultConfigurationForm.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
disable: PropTypes.bool.isRequired,
|
||||||
|
fields: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
updateField: PropTypes.func.isRequired,
|
||||||
|
updateFieldWithKey: PropTypes.func.isRequired,
|
||||||
|
deviceTypes: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddDefaultConfigurationForm;
|
||||||
183
src/components/AddConfigurationModal/index.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CModal, CModalHeader, CModalTitle, CModalBody, CButton, CPopover } from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX, cilSave } from '@coreui/icons';
|
||||||
|
import { useToast, useFormFields, useAuth } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { checkIfJson } from 'utils/helper';
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const initialForm = {
|
||||||
|
name: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
deviceTypes: {
|
||||||
|
value: [],
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddConfigurationModal = ({ show, toggle, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialForm);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deviceTypes, setDeviceTypes] = useState([]);
|
||||||
|
|
||||||
|
const getDeviceTypes = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owfms}/api/v1/firmwares?deviceSet=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setDeviceTypes([...response.data.deviceTypes]);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = () => {
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
for (const [key, field] of Object.entries(fields)) {
|
||||||
|
if (field.required && field.value === '') {
|
||||||
|
updateField(key, { error: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (field.notEmpty && field.value.length === 0) {
|
||||||
|
updateField(key, { error: true, notEmpty: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkIfJson(fields.configuration.value)) {
|
||||||
|
updateField('configuration', { error: true });
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addConfiguration = () => {
|
||||||
|
if (validation()) {
|
||||||
|
setLoading(true);
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
name: fields.name.value,
|
||||||
|
description: fields.description.value,
|
||||||
|
modelIds: fields.deviceTypes.value,
|
||||||
|
configuration: fields.configuration.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.post(
|
||||||
|
`${endpoints.owgw}/api/v1/default_configuration/${fields.name.value}`,
|
||||||
|
parameters,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (refresh !== null) refresh();
|
||||||
|
toggle();
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('configuration.creation_success'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('entity.add_failure', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
getDeviceTypes();
|
||||||
|
setFormFields(initialForm);
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('configuration.create')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.add')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={addConfiguration}>
|
||||||
|
<CIcon content={cilSave} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody className="px-5">
|
||||||
|
<Form
|
||||||
|
t={t}
|
||||||
|
disable={loading}
|
||||||
|
fields={fields}
|
||||||
|
updateField={updateFieldWithId}
|
||||||
|
updateFieldWithKey={updateField}
|
||||||
|
deviceTypes={deviceTypes}
|
||||||
|
show={show}
|
||||||
|
/>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddConfigurationModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddConfigurationModal.defaultProps = {
|
||||||
|
refresh: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddConfigurationModal;
|
||||||
158
src/components/AddToBlacklistModal/index.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CLabel,
|
||||||
|
CTextarea,
|
||||||
|
CInput,
|
||||||
|
CInvalidFeedback,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
|
import { cilPlus, cilX } from '@coreui/icons';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const AddToBlacklistModal = ({ show, toggle, serialNumber, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { endpoints, currentToken } = useAuth();
|
||||||
|
const [chosenSerialNumber, setChosenSerialNumber] = useState('');
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
const addToBlacklist = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
serialNumber: chosenSerialNumber,
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.post(`${endpoints.owgw}/api/v1/blacklist/${chosenSerialNumber}`, parameters, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('device.success_added_blacklist'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggle();
|
||||||
|
if (refresh) refresh();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_adding_blacklist', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
if (serialNumber) setChosenSerialNumber(serialNumber);
|
||||||
|
else setChosenSerialNumber('');
|
||||||
|
}
|
||||||
|
}, [show, serialNumber]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('device.add_to_blacklist')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.add')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={addToBlacklist}
|
||||||
|
disabled={
|
||||||
|
chosenSerialNumber.length !== 12 ||
|
||||||
|
!chosenSerialNumber.match('^[a-fA-F0-9]+$') ||
|
||||||
|
reason === '' ||
|
||||||
|
loading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPlus} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody>
|
||||||
|
<CRow>
|
||||||
|
<CLabel col sm="3">
|
||||||
|
{t('common.serial_number')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="9" className="pt-1">
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={chosenSerialNumber}
|
||||||
|
onChange={(e) => setChosenSerialNumber(e.target.value)}
|
||||||
|
invalid={
|
||||||
|
chosenSerialNumber.length !== 12 && chosenSerialNumber.match('^[a-fA-F0-9]+$')
|
||||||
|
}
|
||||||
|
disabled={loading}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('entity.valid_serial')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CLabel col sm="3">
|
||||||
|
{t('common.reason')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="9" className="pt-2">
|
||||||
|
<CTextarea
|
||||||
|
name="reason"
|
||||||
|
id="reason"
|
||||||
|
rows="3"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddToBlacklistModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
serialNumber: PropTypes.string,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddToBlacklistModal.defaultProps = {
|
||||||
|
serialNumber: '',
|
||||||
|
refresh: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddToBlacklistModal;
|
||||||
210
src/components/BlacklistTable/Table/index.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import {
|
||||||
|
CCardBody,
|
||||||
|
CDataTable,
|
||||||
|
CButton,
|
||||||
|
CLink,
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CButtonToolbar,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { cilSearch, cilPencil, cilPlus, cilTrash } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
|
||||||
|
const BlacklistTable = ({
|
||||||
|
currentPage,
|
||||||
|
devices,
|
||||||
|
toggleAddBlacklist,
|
||||||
|
toggleEditModal,
|
||||||
|
devicesPerPage,
|
||||||
|
loading,
|
||||||
|
removeFromBlacklist,
|
||||||
|
updateDevicesPerPage,
|
||||||
|
pageCount,
|
||||||
|
updatePage,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
|
||||||
|
{ key: 'created', label: t('device.blacklisted_on'), _style: { width: '1%' } },
|
||||||
|
{ key: 'author', label: t('common.by'), filter: false, _style: { width: '15%' } },
|
||||||
|
{ key: 'reason', label: t('common.reason'), filter: false },
|
||||||
|
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hideTooltips = () => ReactTooltip.hide();
|
||||||
|
|
||||||
|
const escFunction = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
hideTooltips();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', escFunction, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CCard className="m-0 p-0">
|
||||||
|
<CCardHeader className="p-0 text-right">
|
||||||
|
<CPopover content={t('device.add_to_blacklist')}>
|
||||||
|
<CButton size="sm" color="primary" onClick={toggleAddBlacklist}>
|
||||||
|
<CIcon content={cilPlus} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
items={devices ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
scopedSlots={{
|
||||||
|
serialNumber: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
{item.serialNumber}
|
||||||
|
</CLink>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
created: (item) => (
|
||||||
|
<td className="text-left align-middle">
|
||||||
|
<div style={{ width: '130px' }}>
|
||||||
|
<FormattedDate date={item.created} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
author: (item) => <td className="align-middle">{item.author}</td>,
|
||||||
|
reason: (item) => <td className="align-middle">{item.reason}</td>,
|
||||||
|
actions: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CButtonToolbar
|
||||||
|
role="group"
|
||||||
|
className="justify-content-center"
|
||||||
|
style={{ width: '130px' }}
|
||||||
|
>
|
||||||
|
<CPopover content={t('configuration.details')}>
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-search" content={cilSearch} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CLink>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('device.remove_from_blacklist')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => removeFromBlacklist(item.serialNumber)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon content={cilTrash} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => toggleEditModal(item.serialNumber)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</CButtonToolbar>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={updatePage}
|
||||||
|
forcePage={Number(currentPage)}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={devicesPerPage}
|
||||||
|
onChange={(e) => updateDevicesPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlacklistTable.propTypes = {
|
||||||
|
currentPage: PropTypes.string,
|
||||||
|
devices: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
toggleAddBlacklist: PropTypes.func.isRequired,
|
||||||
|
toggleEditModal: PropTypes.func.isRequired,
|
||||||
|
updateDevicesPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
updatePage: PropTypes.func.isRequired,
|
||||||
|
devicesPerPage: PropTypes.string.isRequired,
|
||||||
|
removeFromBlacklist: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
BlacklistTable.defaultProps = {
|
||||||
|
currentPage: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(BlacklistTable);
|
||||||
30
src/components/BlacklistTable/Table/index.module.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.firmwareTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipHeader {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-top-left-radius: 1rem !important;
|
||||||
|
border-top-right-radius: 1rem !important;
|
||||||
|
}
|
||||||
244
src/components/BlacklistTable/index.js
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { getItem, setItem } from 'utils/localStorageHelper';
|
||||||
|
import { useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
|
import AddToBlacklistModal from 'components/AddToBlacklistModal';
|
||||||
|
import EditBlacklistModal from 'components/EditBlacklistModal';
|
||||||
|
import Table from './Table';
|
||||||
|
|
||||||
|
const BlacklistTable = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
const [page, setPage] = useState(parseInt(sessionStorage.getItem('deviceTable') ?? 0, 10));
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [deviceCount, setDeviceCount] = useState(0);
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const [devicesPerPage, setDevicesPerPage] = useState(getItem('devicesPerPage') || '10');
|
||||||
|
const [devices, setDevices] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editSerial, setEditSerial] = useState('');
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [showAddModal, toggleAddModal] = useToggle(false);
|
||||||
|
|
||||||
|
const toggleEditModal = (serialNumber) => {
|
||||||
|
if (serialNumber) setEditSerial(serialNumber);
|
||||||
|
setShowEditModal(!showEditModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeviceInformation = (selectedPage = page, devicePerPage = devicesPerPage) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/blacklist?limit=${devicePerPage}&offset=${
|
||||||
|
devicePerPage * selectedPage
|
||||||
|
}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setDevices(response.data.devices);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCount = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/blacklist?countOnly=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const devicesCount = response.data.count;
|
||||||
|
const pagesCount = Math.ceil(devicesCount / devicesPerPage);
|
||||||
|
setPageCount(pagesCount);
|
||||||
|
setDeviceCount(devicesCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= pagesCount) {
|
||||||
|
history.push(`/devices?page=${pagesCount - 1}`);
|
||||||
|
selectedPage = pagesCount - 1;
|
||||||
|
}
|
||||||
|
getDeviceInformation(selectedPage);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDevice = (serialNumber) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let newDevice;
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/blacklist?deviceWithStatus=true&select=${encodeURIComponent(
|
||||||
|
serialNumber,
|
||||||
|
)}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
({
|
||||||
|
data: {
|
||||||
|
devicesWithStatus: [device],
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
|
newDevice = device;
|
||||||
|
|
||||||
|
return axiosInstance.get(
|
||||||
|
`${endpoints.owfms}/api/v1/firmwareAge?select=${serialNumber}`,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
newDevice.firmwareInfo = {
|
||||||
|
age: response.data.ages[0].age,
|
||||||
|
latest: response.data.ages[0].latest,
|
||||||
|
};
|
||||||
|
const foundIndex = devices.findIndex((obj) => obj.serialNumber === serialNumber);
|
||||||
|
const newList = devices;
|
||||||
|
newList[foundIndex] = newDevice;
|
||||||
|
setDevices(newList);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDevicesPerPage = (value) => {
|
||||||
|
setItem('devicesPerPage', value);
|
||||||
|
setDevicesPerPage(value);
|
||||||
|
|
||||||
|
const newPageCount = Math.ceil(deviceCount / value);
|
||||||
|
setPageCount(newPageCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= newPageCount) {
|
||||||
|
history.push(`/blacklist?page=${newPageCount - 1}`);
|
||||||
|
selectedPage = newPageCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeviceInformation(selectedPage, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePageCount = ({ selected: selectedPage }) => {
|
||||||
|
sessionStorage.setItem('deviceTable', selectedPage);
|
||||||
|
setPage(selectedPage);
|
||||||
|
getDeviceInformation(selectedPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromBlacklist = (serialNumber) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.delete(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('device.success_removed_blacklist'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
getCount();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_adding_blacklist', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCount();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
currentPage={page}
|
||||||
|
t={t}
|
||||||
|
devices={devices}
|
||||||
|
loading={loading}
|
||||||
|
toggleAddBlacklist={toggleAddModal}
|
||||||
|
toggleEditModal={toggleEditModal}
|
||||||
|
updateDevicesPerPage={updateDevicesPerPage}
|
||||||
|
devicesPerPage={devicesPerPage}
|
||||||
|
pageCount={pageCount}
|
||||||
|
updatePage={updatePageCount}
|
||||||
|
pageRangeDisplayed={5}
|
||||||
|
refreshDevice={refreshDevice}
|
||||||
|
removeFromBlacklist={removeFromBlacklist}
|
||||||
|
/>
|
||||||
|
{showAddModal ? (
|
||||||
|
<AddToBlacklistModal show={showAddModal} toggle={toggleAddModal} refresh={getCount} />
|
||||||
|
) : null}
|
||||||
|
<EditBlacklistModal
|
||||||
|
show={showEditModal}
|
||||||
|
toggle={toggleEditModal}
|
||||||
|
refresh={getCount}
|
||||||
|
serialNumber={editSerial}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlacklistTable;
|
||||||
@@ -5,21 +5,18 @@ import {
|
|||||||
CModalTitle,
|
CModalTitle,
|
||||||
CModalBody,
|
CModalBody,
|
||||||
CModalFooter,
|
CModalFooter,
|
||||||
CSwitch,
|
|
||||||
CCol,
|
CCol,
|
||||||
CRow,
|
|
||||||
CFormGroup,
|
CFormGroup,
|
||||||
CInputRadio,
|
CInputRadio,
|
||||||
CLabel,
|
CLabel,
|
||||||
CPopover,
|
CPopover,
|
||||||
|
CRow,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilX } from '@coreui/icons';
|
import { cilX } from '@coreui/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DatePicker from 'react-widgets/DatePicker';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { dateToUnix } from 'utils/helper';
|
|
||||||
import 'react-widgets/styles.css';
|
import 'react-widgets/styles.css';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import eventBus from 'utils/eventBus';
|
import eventBus from 'utils/eventBus';
|
||||||
@@ -31,38 +28,24 @@ const BlinkModal = ({ show, toggleModal }) => {
|
|||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const { deviceSerialNumber } = useDevice();
|
const { deviceSerialNumber } = useDevice();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const [isNow, setIsNow] = useState(false);
|
|
||||||
const [waiting, setWaiting] = useState(false);
|
const [waiting, setWaiting] = useState(false);
|
||||||
const [chosenDate, setChosenDate] = useState(new Date().toString());
|
const [chosenPattern, setPattern] = useState('blink');
|
||||||
const [chosenPattern, setPattern] = useState('on');
|
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
|
|
||||||
const toggleNow = () => {
|
|
||||||
setIsNow(!isNow);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setDate = (date) => {
|
|
||||||
if (date) {
|
|
||||||
setChosenDate(date.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show) {
|
if (show) {
|
||||||
setWaiting(false);
|
setWaiting(false);
|
||||||
setChosenDate(new Date().toString());
|
setPattern('blink');
|
||||||
setPattern('on');
|
|
||||||
setResult(null);
|
setResult(null);
|
||||||
}
|
}
|
||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
const doAction = () => {
|
const doAction = () => {
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
const utcDate = new Date(chosenDate);
|
|
||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
serialNumber: deviceSerialNumber,
|
serialNumber: deviceSerialNumber,
|
||||||
when: isNow ? 0 : dateToUnix(utcDate),
|
when: 0,
|
||||||
pattern: chosenPattern,
|
pattern: chosenPattern,
|
||||||
duration: 30,
|
duration: 30,
|
||||||
};
|
};
|
||||||
@@ -113,11 +96,26 @@ const BlinkModal = ({ show, toggleModal }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<CModalBody>
|
<CModalBody>
|
||||||
<CFormGroup row>
|
<CRow className="mb-3">
|
||||||
|
<CCol>{t('blink.explanation')}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CFormGroup row className="mb-0">
|
||||||
<CCol md="3">
|
<CCol md="3">
|
||||||
<CLabel>{t('blink.pattern')}</CLabel>
|
<CLabel>{t('blink.pattern')}</CLabel>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
|
<CFormGroup variant="custom-radio" onClick={() => setPattern('blink')} inline>
|
||||||
|
<CInputRadio
|
||||||
|
custom
|
||||||
|
defaultChecked={chosenPattern === 'blink'}
|
||||||
|
id="radio3"
|
||||||
|
name="radios"
|
||||||
|
value="option3"
|
||||||
|
/>
|
||||||
|
<CLabel variant="custom-checkbox" htmlFor="radio3">
|
||||||
|
{t('blink.blink')}
|
||||||
|
</CLabel>
|
||||||
|
</CFormGroup>
|
||||||
<CFormGroup variant="custom-radio" onClick={() => setPattern('on')} inline>
|
<CFormGroup variant="custom-radio" onClick={() => setPattern('on')} inline>
|
||||||
<CInputRadio
|
<CInputRadio
|
||||||
custom
|
custom
|
||||||
@@ -142,55 +140,12 @@ const BlinkModal = ({ show, toggleModal }) => {
|
|||||||
{t('common.off')}
|
{t('common.off')}
|
||||||
</CLabel>
|
</CLabel>
|
||||||
</CFormGroup>
|
</CFormGroup>
|
||||||
<CFormGroup variant="custom-radio" onClick={() => setPattern('blink')} inline>
|
|
||||||
<CInputRadio
|
|
||||||
custom
|
|
||||||
defaultChecked={chosenPattern === 'blink'}
|
|
||||||
id="radio3"
|
|
||||||
name="radios"
|
|
||||||
value="option3"
|
|
||||||
/>
|
|
||||||
<CLabel variant="custom-checkbox" htmlFor="radio3">
|
|
||||||
{t('blink.blink')}
|
|
||||||
</CLabel>
|
|
||||||
</CFormGroup>
|
|
||||||
</CCol>
|
</CCol>
|
||||||
</CFormGroup>
|
</CFormGroup>
|
||||||
<CRow className="pt-1">
|
|
||||||
<CCol md="8">
|
|
||||||
<p>{t('blink.execute_now')}</p>
|
|
||||||
</CCol>
|
|
||||||
<CCol>
|
|
||||||
<CSwitch
|
|
||||||
disabled={waiting}
|
|
||||||
color="primary"
|
|
||||||
defaultChecked={isNow}
|
|
||||||
onClick={toggleNow}
|
|
||||||
labelOn={t('common.yes')}
|
|
||||||
labelOff={t('common.no')}
|
|
||||||
/>
|
|
||||||
</CCol>
|
|
||||||
</CRow>
|
|
||||||
<CRow hidden={isNow} className="pt-3">
|
|
||||||
<CCol md="4" className="pt-2">
|
|
||||||
<p>{t('common.custom_date')}</p>
|
|
||||||
</CCol>
|
|
||||||
<CCol xs="12" md="8">
|
|
||||||
<DatePicker
|
|
||||||
selected={new Date(chosenDate)}
|
|
||||||
includeTime
|
|
||||||
value={new Date(chosenDate)}
|
|
||||||
placeholder="Select custom date"
|
|
||||||
disabled={waiting}
|
|
||||||
onChange={(date) => setDate(date)}
|
|
||||||
min={new Date()}
|
|
||||||
/>
|
|
||||||
</CCol>
|
|
||||||
</CRow>
|
|
||||||
</CModalBody>
|
</CModalBody>
|
||||||
<CModalFooter>
|
<CModalFooter>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
label={isNow ? t('blink.set_leds') : t('common.schedule')}
|
label={t('blink.set_leds')}
|
||||||
isLoadingLabel={t('common.loading_ellipsis')}
|
isLoadingLabel={t('common.loading_ellipsis')}
|
||||||
isLoading={waiting}
|
isLoading={waiting}
|
||||||
action={doAction}
|
action={doAction}
|
||||||
|
|||||||
100
src/components/CapabilitiesDisplay/index.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CLabel,
|
||||||
|
CPopover,
|
||||||
|
CSpinner,
|
||||||
|
CButton,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilSync } from '@coreui/icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CopyToClipboardButton, useAuth, useToast, FormattedDate } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const CapabilitiesDisplay = ({ serialNumber }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [capabilities, setCapabilities] = useState({});
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const getCapabilities = () => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(serialNumber)}/capabilities`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setCapabilities(response.data);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCapabilities();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CCard className="m-0">
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.refresh')}>
|
||||||
|
<CButton size="sm" color="info" onClick={getCapabilities}>
|
||||||
|
<CIcon content={cilSync} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<h5>
|
||||||
|
{t('device.capabilities')}
|
||||||
|
<CopyToClipboardButton
|
||||||
|
t={t}
|
||||||
|
size="sm"
|
||||||
|
content={JSON.stringify(capabilities?.capabilities ?? {})}
|
||||||
|
/>
|
||||||
|
</h5>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CLabel>
|
||||||
|
{t('inventory.last_modification')}: <FormattedDate date={capabilities?.lastUpdate} />
|
||||||
|
</CLabel>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
{loading ? <CSpinner /> : null}
|
||||||
|
<pre className="ignore">{JSON.stringify(capabilities?.capabilities ?? {}, null, 4)}</pre>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CapabilitiesDisplay.propTypes = {
|
||||||
|
serialNumber: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CapabilitiesDisplay;
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
CWidgetDropdown,
|
CCardHeader,
|
||||||
CRow,
|
CCardBody,
|
||||||
CCol,
|
|
||||||
CButton,
|
CButton,
|
||||||
CDataTable,
|
CDataTable,
|
||||||
CCard,
|
CCard,
|
||||||
CPopover,
|
CPopover,
|
||||||
CButtonToolbar,
|
CButtonToolbar,
|
||||||
|
CFormText,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import DatePicker from 'react-widgets/DatePicker';
|
import DatePicker from 'react-widgets/DatePicker';
|
||||||
@@ -41,7 +41,9 @@ const DeviceCommands = () => {
|
|||||||
const [commands, setCommands] = useState([]);
|
const [commands, setCommands] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [start, setStart] = useState('');
|
const [start, setStart] = useState('');
|
||||||
|
const [startError, setStartError] = useState(false);
|
||||||
const [end, setEnd] = useState('');
|
const [end, setEnd] = useState('');
|
||||||
|
const [endError, setEndError] = useState(false);
|
||||||
const [commandLimit, setCommandLimit] = useState(25);
|
const [commandLimit, setCommandLimit] = useState(25);
|
||||||
// Load more button related
|
// Load more button related
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
@@ -65,11 +67,25 @@ const DeviceCommands = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const modifyStart = (value) => {
|
const modifyStart = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setStartError(false);
|
||||||
setStart(value);
|
setStart(value);
|
||||||
|
} catch (e) {
|
||||||
|
setStart('');
|
||||||
|
setStartError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyEnd = (value) => {
|
const modifyEnd = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setEndError(false);
|
||||||
setEnd(value);
|
setEnd(value);
|
||||||
|
} catch (e) {
|
||||||
|
setEnd('');
|
||||||
|
setEndError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCommandFromList = (commandUuid) => {
|
const deleteCommandFromList = (commandUuid) => {
|
||||||
@@ -245,25 +261,47 @@ const DeviceCommands = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CWidgetDropdown
|
<CCard className="m-0">
|
||||||
className="m-0"
|
<CCardHeader className="dark-header">
|
||||||
inverse="true"
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
color="gradient-primary"
|
<div className="pl-2">
|
||||||
header={t('commands.title')}
|
<CPopover content={t('common.refresh')}>
|
||||||
footerSlot={
|
<CButton
|
||||||
<div className="pb-1 px-3">
|
size="sm"
|
||||||
<CRow className="mb-2">
|
color="info"
|
||||||
<CCol>
|
onClick={getCommands}
|
||||||
From:
|
disabled={startError || endError}
|
||||||
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
|
>
|
||||||
</CCol>
|
<CIcon content={cilSync} />
|
||||||
<CCol>
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
<div className="pl-2">
|
||||||
|
<DatePicker
|
||||||
|
includeTime
|
||||||
|
onChange={(date) => modifyEnd(date)}
|
||||||
|
value={end ? new Date(end) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!endError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
To:
|
To:
|
||||||
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
|
<div className="pl-2">
|
||||||
</CCol>
|
<DatePicker
|
||||||
</CRow>
|
includeTime
|
||||||
<CCard>
|
onChange={(date) => modifyStart(date)}
|
||||||
<div className="overflow-auto" style={{ height: '200px' }}>
|
value={start ? new Date(start) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!startError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
From:
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
|
||||||
<CDataTable
|
<CDataTable
|
||||||
addTableClasses="ignore-overflow table-sm"
|
addTableClasses="ignore-overflow table-sm"
|
||||||
border
|
border
|
||||||
@@ -325,17 +363,9 @@ const DeviceCommands = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.command === 'trace' ? (
|
{item.command === 'trace' ? (
|
||||||
<CIcon
|
<CIcon name="cil-cloud-download" content={cilCloudDownload} />
|
||||||
name="cil-cloud-download"
|
|
||||||
content={cilCloudDownload}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<CIcon
|
<CIcon name="cil-calendar-check" content={cilCalendarCheck} />
|
||||||
name="cil-calendar-check"
|
|
||||||
content={cilCalendarCheck}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
@@ -350,7 +380,7 @@ const DeviceCommands = () => {
|
|||||||
toggleResponse(item);
|
toggleResponse(item);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CIcon name="cilList" size="md" />
|
<CIcon name="cilList" />
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
<CPopover content={t('common.delete')}>
|
<CPopover content={t('common.delete')}>
|
||||||
@@ -364,7 +394,7 @@ const DeviceCommands = () => {
|
|||||||
toggleConfirmModal(item.UUID, index);
|
toggleConfirmModal(item.UUID, index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CIcon name="cilTrash" size="mdå" />
|
<CIcon name="cilTrash" />
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
</CButtonToolbar>
|
</CButtonToolbar>
|
||||||
@@ -385,17 +415,8 @@ const DeviceCommands = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</CCardBody>
|
||||||
</CCard>
|
</CCard>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="text-right float-right">
|
|
||||||
<CButton onClick={refreshCommands} size="sm">
|
|
||||||
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
|
|
||||||
</CButton>
|
|
||||||
</div>
|
|
||||||
</CWidgetDropdown>
|
|
||||||
|
|
||||||
<WifiScanResultModalWidget
|
<WifiScanResultModalWidget
|
||||||
show={showScanModal}
|
show={showScanModal}
|
||||||
toggle={toggleScanModal}
|
toggle={toggleScanModal}
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ const ConfigurationDisplay = ({ getData, deviceConfig }) => {
|
|||||||
/>
|
/>
|
||||||
</h5>
|
</h5>
|
||||||
<CRow>
|
<CRow>
|
||||||
<CCol md="2" xl="2" xxl="1">
|
<CCol>
|
||||||
<CLabel>{t('configuration.last_configuration_change')}: </CLabel>
|
<CLabel>
|
||||||
|
{t('configuration.last_configuration_change')}:{' '}
|
||||||
|
{prettyDate(deviceConfig?.lastConfigurationChange)}
|
||||||
|
</CLabel>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>{prettyDate(deviceConfig?.lastConfigurationChange)}</CCol>
|
|
||||||
</CRow>
|
</CRow>
|
||||||
<pre className="ignore">{JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}</pre>
|
<pre className="ignore">{JSON.stringify(deviceConfig?.configuration ?? {}, null, 4)}</pre>
|
||||||
</CCardBody>
|
</CCardBody>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import 'react-widgets/styles.css';
|
import 'react-widgets/styles.css';
|
||||||
import { useAuth, useDevice } from 'ucentral-libs';
|
import { useAuth, useDevice, useToast } from 'ucentral-libs';
|
||||||
import { checkIfJson } from 'utils/helper';
|
import { checkIfJson } from 'utils/helper';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import eventBus from 'utils/eventBus';
|
import eventBus from 'utils/eventBus';
|
||||||
@@ -29,6 +29,7 @@ import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
|
|||||||
const ConfigureModal = ({ show, toggleModal }) => {
|
const ConfigureModal = ({ show, toggleModal }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const { addToast } = useToast();
|
||||||
const { deviceSerialNumber } = useDevice();
|
const { deviceSerialNumber } = useDevice();
|
||||||
const [hadSuccess, setHadSuccess] = useState(false);
|
const [hadSuccess, setHadSuccess] = useState(false);
|
||||||
const [hadFailure, setHadFailure] = useState(false);
|
const [hadFailure, setHadFailure] = useState(false);
|
||||||
@@ -91,7 +92,13 @@ const ConfigureModal = ({ show, toggleModal }) => {
|
|||||||
{ headers },
|
{ headers },
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setHadSuccess(true);
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('commands.command_success'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggleModal();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setResponseBody('Error while submitting command!');
|
setResponseBody('Error while submitting command!');
|
||||||
|
|||||||
@@ -1,161 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { CModal, CModalHeader, CModalBody, CModalTitle, CPopover, CButton } from '@coreui/react';
|
|
||||||
import CIcon from '@coreui/icons-react';
|
|
||||||
import { cilSave, cilX } from '@coreui/icons';
|
|
||||||
import { CreateUserForm, useFormFields, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { testRegex, validateEmail } from 'utils/helper';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
name: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
currentPassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
changePassword: {
|
|
||||||
value: 'on',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
userRole: {
|
|
||||||
value: 'accounting',
|
|
||||||
error: false,
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateUserModal = ({ show, toggle, getUsers, policies }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { addToast } = useToast();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [formFields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialState);
|
|
||||||
|
|
||||||
const toggleChange = () => {
|
|
||||||
updateField('changePassword', { value: !formFields.changePassword.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const createUser = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
id: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let validationSuccess = true;
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(formFields)) {
|
|
||||||
if (!value.optional && value.value === '') {
|
|
||||||
validationSuccess = false;
|
|
||||||
updateField(key, { value: value.value, error: true });
|
|
||||||
} else if (key === 'currentPassword' && !testRegex(value.value, policies.passwordPattern)) {
|
|
||||||
validationSuccess = false;
|
|
||||||
updateField(key, { value: value.value, error: true });
|
|
||||||
} else if (key === 'email' && !validateEmail(value.value)) {
|
|
||||||
validationSuccess = false;
|
|
||||||
updateField(key, { value: value.value, error: true });
|
|
||||||
} else if (key === 'notes') {
|
|
||||||
parameters[key] = [{ note: value.value }];
|
|
||||||
} else if (key === 'changePassword') {
|
|
||||||
parameters[key] = value.value === 'on';
|
|
||||||
} else {
|
|
||||||
parameters[key] = value.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validationSuccess) {
|
|
||||||
const headers = {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.post(`${endpoints.owsec}/api/v1/user/0`, parameters, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
getUsers();
|
|
||||||
setFormFields(initialState);
|
|
||||||
addToast({
|
|
||||||
title: t('common.success'),
|
|
||||||
body: t('user.create_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
toggle();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.create_failure', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
useEffect(() => {
|
|
||||||
setFormFields(initialState);
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CModal show={show} onClose={toggle} size="xl">
|
|
||||||
<CModalHeader className="p-1">
|
|
||||||
<CModalTitle className="pl-1 pt-1">{t('user.create')}</CModalTitle>
|
|
||||||
<div className="text-right">
|
|
||||||
<CPopover content={t('user.create')}>
|
|
||||||
<CButton color="primary" variant="outline" onClick={createUser} disabled={loading}>
|
|
||||||
<CIcon content={cilSave} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
<CPopover content={t('common.close')}>
|
|
||||||
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
|
||||||
<CIcon content={cilX} />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
</div>
|
|
||||||
</CModalHeader>
|
|
||||||
<CModalBody>
|
|
||||||
<CreateUserForm
|
|
||||||
t={t}
|
|
||||||
fields={formFields}
|
|
||||||
updateField={updateFieldWithId}
|
|
||||||
policies={policies}
|
|
||||||
toggleChange={toggleChange}
|
|
||||||
/>
|
|
||||||
</CModalBody>
|
|
||||||
</CModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CreateUserModal.propTypes = {
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
toggle: PropTypes.func.isRequired,
|
|
||||||
getUsers: PropTypes.func.isRequired,
|
|
||||||
policies: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(CreateUserModal);
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import { CButton, CCardBody, CCardHeader, CRow, CCol, CPopover, CButtonClose } from '@coreui/react';
|
||||||
|
import { cilTrash } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { LoadingButton } from 'ucentral-libs';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const DeleteButton = ({ t, config, deleteConfig, hideTooltips }) => {
|
||||||
|
const [tooltipId] = useState(createUuid());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CPopover content={t('common.delete')}>
|
||||||
|
<div className="d-inline">
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-2"
|
||||||
|
data-tip
|
||||||
|
data-for={tooltipId}
|
||||||
|
data-event="click"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-trash" content={cilTrash} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
<ReactTooltip
|
||||||
|
id={tooltipId}
|
||||||
|
place="top"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
clickable
|
||||||
|
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
|
||||||
|
border
|
||||||
|
borderColor="#321fdb"
|
||||||
|
arrowColor="white"
|
||||||
|
overridePosition={({ left, top }) => {
|
||||||
|
const element = document.getElementById(tooltipId);
|
||||||
|
const tooltipWidth = element ? element.offsetWidth : 0;
|
||||||
|
const newLeft = left - tooltipWidth * 0.25;
|
||||||
|
return { top, left: newLeft };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CCardHeader color="primary" className={styles.tooltipHeader}>
|
||||||
|
{t('configuration.delete_config')}
|
||||||
|
<CButtonClose
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="py-1 px-4">
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<LoadingButton
|
||||||
|
data-toggle="dropdown"
|
||||||
|
variant="outline"
|
||||||
|
color="danger"
|
||||||
|
label={t('common.confirm')}
|
||||||
|
isLoadingLabel={t('user.deleting')}
|
||||||
|
isLoading={false}
|
||||||
|
action={() => deleteConfig(config.name)}
|
||||||
|
block
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
</ReactTooltip>
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeleteButton.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
config: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
deleteConfig: PropTypes.func.isRequired,
|
||||||
|
hideTooltips: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteButton;
|
||||||
184
src/components/DefaultConfigurationTable/Table/index.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import {
|
||||||
|
CCardBody,
|
||||||
|
CDataTable,
|
||||||
|
CButton,
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CButtonToolbar,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { cilPencil, cilPlus } from '@coreui/icons';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
import DeleteButton from './DeleteButton';
|
||||||
|
|
||||||
|
const DefaultConfigurationTable = ({
|
||||||
|
currentPage,
|
||||||
|
configurations,
|
||||||
|
toggleAddBlacklist,
|
||||||
|
toggleEditModal,
|
||||||
|
configurationsPerPage,
|
||||||
|
loading,
|
||||||
|
deleteConfig,
|
||||||
|
updateDevicesPerPage,
|
||||||
|
pageCount,
|
||||||
|
updatePage,
|
||||||
|
t,
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'name', label: t('user.name'), _style: { width: '20%' } },
|
||||||
|
{ key: 'description', label: t('user.description'), _style: { width: '20%' } },
|
||||||
|
{ key: 'created', label: t('common.created'), _style: { width: '10%' } },
|
||||||
|
{ key: 'modified', label: t('common.modified'), _style: { width: '10%' } },
|
||||||
|
{ key: 'deviceTypes', label: t('firmware.device_types'), _style: { width: '20%' } },
|
||||||
|
{ key: 'actions', label: t('actions.actions'), _style: { width: '1%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hideTooltips = () => ReactTooltip.hide();
|
||||||
|
|
||||||
|
const escFunction = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
hideTooltips();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', escFunction, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CCard className="m-0 p-0">
|
||||||
|
<CCardHeader className="dark-header text-right">
|
||||||
|
<div className="text-value-lg float-left">
|
||||||
|
{t('configuration.default_configurations')}
|
||||||
|
</div>
|
||||||
|
<div className="text-right float-right">
|
||||||
|
<CPopover content={t('configuration.create_config')}>
|
||||||
|
<CButton size="sm" color="info" onClick={toggleAddBlacklist}>
|
||||||
|
<CIcon content={cilPlus} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
items={configurations ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
scopedSlots={{
|
||||||
|
name: (item) => <td className="align-middle">{item.name}</td>,
|
||||||
|
description: (item) => <td className="align-middle">{item.description}</td>,
|
||||||
|
deviceTypes: (item) => <td className="align-middle">{item.modelIds.join(', ')}</td>,
|
||||||
|
created: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<FormattedDate date={item.created} />
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
modified: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<FormattedDate date={item.lastModified} />
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
actions: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CButtonToolbar
|
||||||
|
role="group"
|
||||||
|
className="justify-content-center"
|
||||||
|
style={{ width: '90px' }}
|
||||||
|
>
|
||||||
|
<DeleteButton
|
||||||
|
t={t}
|
||||||
|
config={item}
|
||||||
|
deleteConfig={deleteConfig}
|
||||||
|
hideTooltips={hideTooltips}
|
||||||
|
/>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => toggleEditModal(item.name)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</CButtonToolbar>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={updatePage}
|
||||||
|
forcePage={Number(currentPage)}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={configurationsPerPage}
|
||||||
|
onChange={(e) => updateDevicesPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultConfigurationTable.propTypes = {
|
||||||
|
currentPage: PropTypes.string,
|
||||||
|
configurations: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
toggleAddBlacklist: PropTypes.func.isRequired,
|
||||||
|
toggleEditModal: PropTypes.func.isRequired,
|
||||||
|
updateDevicesPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
updatePage: PropTypes.func.isRequired,
|
||||||
|
configurationsPerPage: PropTypes.string.isRequired,
|
||||||
|
deleteConfig: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DefaultConfigurationTable.defaultProps = {
|
||||||
|
currentPage: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DefaultConfigurationTable);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
.firmwareTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipHeader {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-top-left-radius: 1rem !important;
|
||||||
|
border-top-right-radius: 1rem !important;
|
||||||
|
}
|
||||||
199
src/components/DefaultConfigurationTable/index.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { getItem, setItem } from 'utils/localStorageHelper';
|
||||||
|
import { useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
|
import AddConfigurationModal from 'components/AddConfigurationModal';
|
||||||
|
import EditConfigurationModal from 'components/EditConfigurationModal';
|
||||||
|
import Table from './Table';
|
||||||
|
|
||||||
|
const DefaultConfigurationTable = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
const [page, setPage] = useState(parseInt(sessionStorage.getItem('configurationTable') ?? 0, 10));
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [configurationCount, setConfigurationCount] = useState(0);
|
||||||
|
const [pageCount, setPageCount] = useState(0);
|
||||||
|
const [configurationsPerPage, setConfigurationsPerPage] = useState(
|
||||||
|
getItem('configurationsPerPage') || '10',
|
||||||
|
);
|
||||||
|
const [configurations, setConfigurations] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [editId, setEditId] = useState('');
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [showAddModal, toggleAddModal] = useToggle(false);
|
||||||
|
|
||||||
|
const toggleEditModal = (serialNumber) => {
|
||||||
|
if (serialNumber) setEditId(serialNumber);
|
||||||
|
setShowEditModal(!showEditModal);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getConfigurationInformation = (
|
||||||
|
selectedPage = page,
|
||||||
|
configurationPerPage = configurationsPerPage,
|
||||||
|
) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/default_configurations?limit=${configurationPerPage}&offset=${
|
||||||
|
configurationPerPage * selectedPage
|
||||||
|
}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
setConfigurations(response.data.configurations);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_fetching_configurations', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCount = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/default_configurations?countOnly=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const configurationsCount = response.data.count;
|
||||||
|
const pagesCount = Math.ceil(configurationsCount / configurationsPerPage);
|
||||||
|
setPageCount(pagesCount);
|
||||||
|
setConfigurationCount(configurationsCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= pagesCount) {
|
||||||
|
history.push(`/defaultconfigurations?page=${pagesCount - 1}`);
|
||||||
|
selectedPage = pagesCount - 1;
|
||||||
|
}
|
||||||
|
getConfigurationInformation(selectedPage);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_fetching_configurations', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConfigurationsPerPage = (value) => {
|
||||||
|
setItem('configurationsPerPage', value);
|
||||||
|
setConfigurationsPerPage(value);
|
||||||
|
|
||||||
|
const newPageCount = Math.ceil(configurationCount / value);
|
||||||
|
setPageCount(newPageCount);
|
||||||
|
|
||||||
|
let selectedPage = page;
|
||||||
|
|
||||||
|
if (page >= newPageCount) {
|
||||||
|
history.push(`/default_configurations?page=${newPageCount - 1}`);
|
||||||
|
selectedPage = newPageCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigurationInformation(selectedPage, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePageCount = ({ selected: selectedPage }) => {
|
||||||
|
sessionStorage.setItem('configurationTable', selectedPage);
|
||||||
|
setPage(selectedPage);
|
||||||
|
getConfigurationInformation(selectedPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteConfig = (name) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.delete(`${endpoints.owgw}/api/v1/default_configuration/${name}`, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('configuration.successful_delete'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
getCount();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_adding_blacklist', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCount();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
currentPage={page}
|
||||||
|
t={t}
|
||||||
|
configurations={configurations}
|
||||||
|
loading={loading}
|
||||||
|
toggleAddBlacklist={toggleAddModal}
|
||||||
|
toggleEditModal={toggleEditModal}
|
||||||
|
updateConfigurationsPerPage={updateConfigurationsPerPage}
|
||||||
|
configurationsPerPage={configurationsPerPage}
|
||||||
|
pageCount={pageCount}
|
||||||
|
updatePage={updatePageCount}
|
||||||
|
pageRangeDisplayed={5}
|
||||||
|
deleteConfig={deleteConfig}
|
||||||
|
/>
|
||||||
|
{showAddModal ? (
|
||||||
|
<AddConfigurationModal show={showAddModal} toggle={toggleAddModal} refresh={getCount} />
|
||||||
|
) : null}
|
||||||
|
<EditConfigurationModal
|
||||||
|
show={showEditModal}
|
||||||
|
toggle={toggleEditModal}
|
||||||
|
refresh={getCount}
|
||||||
|
configId={editId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultConfigurationTable;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react';
|
import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { LoadingButton, useAuth, useDevice, useToast } from 'ucentral-libs';
|
import { LoadingButton, useAuth, useDevice, useToast, useToggle } from 'ucentral-libs';
|
||||||
import RebootModal from 'components/RebootModal';
|
import RebootModal from 'components/RebootModal';
|
||||||
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
||||||
import ConfigureModal from 'components/ConfigureModal';
|
import ConfigureModal from 'components/ConfigureModal';
|
||||||
@@ -13,7 +14,7 @@ import FactoryResetModal from 'components/FactoryResetModal';
|
|||||||
import EventQueueModal from 'components/EventQueueModal';
|
import EventQueueModal from 'components/EventQueueModal';
|
||||||
import TelemetryModal from 'components/TelemetryModal';
|
import TelemetryModal from 'components/TelemetryModal';
|
||||||
|
|
||||||
const DeviceActions = () => {
|
const DeviceActions = ({ device }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
@@ -21,35 +22,16 @@ const DeviceActions = () => {
|
|||||||
const [upgradeStatus, setUpgradeStatus] = useState({
|
const [upgradeStatus, setUpgradeStatus] = useState({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
const [device, setDevice] = useState({});
|
|
||||||
const [showRebootModal, setShowRebootModal] = useState(false);
|
|
||||||
const [showBlinkModal, setShowBlinkModal] = useState(false);
|
|
||||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
|
||||||
const [showTraceModal, setShowTraceModal] = useState(false);
|
|
||||||
const [showScanModal, setShowScanModal] = useState(false);
|
|
||||||
const [connectLoading, setConnectLoading] = useState(false);
|
const [connectLoading, setConnectLoading] = useState(false);
|
||||||
const [showConfigModal, setConfigModal] = useState(false);
|
const [showRebootModal, toggleRebootModal] = useToggle(false);
|
||||||
const [showFactoryModal, setShowFactoryModal] = useState(false);
|
const [showBlinkModal, toggleBlinkModal] = useToggle(false);
|
||||||
const [showQueueModal, setShowQueueModal] = useState(false);
|
const [showUpgradeModal, toggleUpgradeModal, setShowUpgradeModal] = useToggle(false);
|
||||||
const [showTelemetryModal, setShowTelemetryModal] = useState(false);
|
const [showTraceModal, toggleTraceModal] = useToggle(false);
|
||||||
|
const [showScanModal, toggleScanModal] = useToggle(false);
|
||||||
const toggleRebootModal = () => setShowRebootModal(!showRebootModal);
|
const [showConfigModal, toggleConfigModal] = useToggle(false);
|
||||||
|
const [showFactoryModal, toggleFactoryResetModal] = useToggle(false);
|
||||||
const toggleBlinkModal = () => setShowBlinkModal(!showBlinkModal);
|
const [showQueueModal, toggleQueueModal] = useToggle(false);
|
||||||
|
const [showTelemetryModal, toggleTelemetryModal] = useToggle(false);
|
||||||
const toggleUpgradeModal = () => setShowUpgradeModal(!showUpgradeModal);
|
|
||||||
|
|
||||||
const toggleTraceModal = () => setShowTraceModal(!showTraceModal);
|
|
||||||
|
|
||||||
const toggleScanModal = () => setShowScanModal(!showScanModal);
|
|
||||||
|
|
||||||
const toggleConfigModal = () => setConfigModal(!showConfigModal);
|
|
||||||
|
|
||||||
const toggleFactoryResetModal = () => setShowFactoryModal(!showFactoryModal);
|
|
||||||
|
|
||||||
const toggleQueueModal = () => setShowQueueModal(!showQueueModal);
|
|
||||||
|
|
||||||
const toggleTelemetryModal = () => setShowTelemetryModal(!showTelemetryModal);
|
|
||||||
|
|
||||||
const getRttysInfo = () => {
|
const getRttysInfo = () => {
|
||||||
setConnectLoading(true);
|
setConnectLoading(true);
|
||||||
@@ -67,6 +49,7 @@ const DeviceActions = () => {
|
|||||||
)
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`;
|
const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`;
|
||||||
|
|
||||||
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
|
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
if (newWindow) newWindow.opener = null;
|
if (newWindow) newWindow.opener = null;
|
||||||
})
|
})
|
||||||
@@ -83,22 +66,6 @@ const DeviceActions = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceInformation = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
setDevice(response.data);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (upgradeStatus.result !== undefined) {
|
if (upgradeStatus.result !== undefined) {
|
||||||
addToast({
|
addToast({
|
||||||
@@ -116,10 +83,6 @@ const DeviceActions = () => {
|
|||||||
}
|
}
|
||||||
}, [upgradeStatus]);
|
}, [upgradeStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getDeviceInformation();
|
|
||||||
}, [deviceSerialNumber]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CCard>
|
<CCard>
|
||||||
<CCardHeader className="dark-header">
|
<CCardHeader className="dark-header">
|
||||||
@@ -128,36 +91,41 @@ const DeviceActions = () => {
|
|||||||
<CCardBody>
|
<CCardBody>
|
||||||
<CRow>
|
<CRow>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block onClick={toggleRebootModal} color="primary">
|
<CButton block disabled={device === null} onClick={toggleRebootModal} color="primary">
|
||||||
{t('actions.reboot')}
|
{t('actions.reboot')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block onClick={toggleBlinkModal} color="primary">
|
<CButton block disabled={device === null} onClick={toggleBlinkModal} color="primary">
|
||||||
{t('actions.blink')}
|
{t('actions.blink')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleUpgradeModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleUpgradeModal}>
|
||||||
{t('actions.firmware_upgrade')}
|
{t('actions.firmware_upgrade')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleTraceModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleTraceModal}>
|
||||||
{t('actions.trace')}
|
{t('actions.trace')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleScanModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleScanModal}>
|
||||||
{t('actions.wifi_scan')}
|
{t('actions.wifi_scan')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleFactoryResetModal}>
|
<CButton
|
||||||
|
block
|
||||||
|
disabled={device === null}
|
||||||
|
color="primary"
|
||||||
|
onClick={toggleFactoryResetModal}
|
||||||
|
>
|
||||||
{t('actions.factory_reset')}
|
{t('actions.factory_reset')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
@@ -165,6 +133,7 @@ const DeviceActions = () => {
|
|||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
|
disabled={device === null}
|
||||||
isLoading={connectLoading}
|
isLoading={connectLoading}
|
||||||
label={t('actions.connect')}
|
label={t('actions.connect')}
|
||||||
isLoadingLabel={t('actions.connecting')}
|
isLoadingLabel={t('actions.connecting')}
|
||||||
@@ -172,19 +141,24 @@ const DeviceActions = () => {
|
|||||||
/>
|
/>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleConfigModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleConfigModal}>
|
||||||
{t('actions.configure')}
|
{t('actions.configure')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
<CRow className="my-1">
|
<CRow className="my-1">
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleQueueModal}>
|
<CButton block disabled={device === null} color="primary" onClick={toggleQueueModal}>
|
||||||
{t('commands.event_queue')}
|
{t('commands.event_queue')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CButton block color="primary" onClick={toggleTelemetryModal}>
|
<CButton
|
||||||
|
block
|
||||||
|
disabled={device === null}
|
||||||
|
color="primary"
|
||||||
|
onClick={toggleTelemetryModal}
|
||||||
|
>
|
||||||
{t('actions.telemetry')}
|
{t('actions.telemetry')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</CCol>
|
||||||
@@ -212,4 +186,12 @@ const DeviceActions = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
DeviceActions.propTypes = {
|
||||||
|
device: PropTypes.instanceOf(Object),
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceActions.defaultProps = {
|
||||||
|
device: null,
|
||||||
|
};
|
||||||
|
|
||||||
export default DeviceActions;
|
export default DeviceActions;
|
||||||
|
|||||||
418
src/components/DeviceDashboard/Dashboard/index.js
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CCol,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CSpinner,
|
||||||
|
CWidgetIcon,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CChartBar, CChartHorizontalBar, CChartPie } from '@coreui/react-chartjs';
|
||||||
|
import { cilClock, cilInfo, cilMedicalCross, cilThumbUp, cilWarning } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const getColor = (health) => {
|
||||||
|
const numberHealth = health ? Number(health.replace('%', '')) : 0;
|
||||||
|
if (numberHealth >= 90) return 'success';
|
||||||
|
if (numberHealth >= 60) return 'warning';
|
||||||
|
return 'danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = (health) => {
|
||||||
|
const numberHealth = health ? Number(health.replace('%', '')) : 0;
|
||||||
|
if (numberHealth >= 90) return <CIcon width={36} name="cil-thumbs-up" content={cilThumbUp} />;
|
||||||
|
if (numberHealth >= 60) return <CIcon width={36} name="cil-warning" content={cilWarning} />;
|
||||||
|
return <CIcon width={36} name="cil-medical-cross" content={cilMedicalCross} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeviceDashboard = ({ t, data, loading }) => (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{ opacity: loading ? '20%' : '100%' }}>
|
||||||
|
<CRow className="mt-3">
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.last_dashboard_refresh')}
|
||||||
|
header={data.snapshot ? <FormattedDate date={data.snapshot} size="lg" /> : <h2>-</h2>}
|
||||||
|
color="info"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-clock" content={cilClock} />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.overall_health')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.health_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header={<h2>{data.overallHealth}</h2>}
|
||||||
|
color={getColor(data.overallHealth)}
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
{getIcon(data.overallHealth)}
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.devices')}
|
||||||
|
header={<h2>{data.numberOfDevices}</h2>}
|
||||||
|
color="primary"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-router" />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.device_status')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.status.datasets}
|
||||||
|
labels={data.status.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.device_health')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.health_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.healths.datasets}
|
||||||
|
labels={data.healths.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]}${t('common.of_connected')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
{data.totalAssociations}{' '}
|
||||||
|
{data.totalAssociations === 1
|
||||||
|
? t('wifi_analysis.association')
|
||||||
|
: t('wifi_analysis.associations')}
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.associations.datasets}
|
||||||
|
labels={data.associations.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]}% of ${
|
||||||
|
data.totalAssociations
|
||||||
|
} associations`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.vendors')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartHorizontalBar
|
||||||
|
datasets={data.vendors.datasets}
|
||||||
|
labels={data.vendors.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => value.split(' ')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('firmware.device_types')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.deviceType.datasets}
|
||||||
|
labels={data.deviceType.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.uptimes')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.uptimes_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.upTimes.datasets}
|
||||||
|
labels={data.upTimes.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.certificates')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.certificate_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.certificates.datasets}
|
||||||
|
labels={data.certificates.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]}${t('common.of_connected')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.commands')}</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.commands.datasets}
|
||||||
|
labels={data.commands.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol lg="6" xl="4">
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.memory_used')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('device.memory_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.memoryUsed.datasets}
|
||||||
|
labels={data.memoryUsed.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.centerContainer}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
DeviceDashboard.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceDashboard);
|
||||||
10
src/components/DeviceDashboard/Dashboard/index.module.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.centerContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DeviceDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
|
import { useAuth, COLOR_LIST } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
const DeviceDashboard = () => {
|
const DeviceDashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
145
src/components/DeviceFirmwareModal/Modal/index.js
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CDataTable,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CInput,
|
||||||
|
CPopover,
|
||||||
|
CSwitch,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX } from '@coreui/icons';
|
||||||
|
import { LoadingButton } from 'ucentral-libs';
|
||||||
|
import { cleanBytesString, prettyDate } from 'utils/helper';
|
||||||
|
|
||||||
|
const DeviceFirmwareModal = ({
|
||||||
|
t,
|
||||||
|
device,
|
||||||
|
show,
|
||||||
|
toggle,
|
||||||
|
firmwareVersions,
|
||||||
|
upgradeToVersion,
|
||||||
|
loading,
|
||||||
|
upgradeStatus,
|
||||||
|
keepRedirector,
|
||||||
|
toggleRedirector,
|
||||||
|
}) => {
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ key: 'imageDate', label: t('firmware.image_date'), _style: { width: '17%' }, filter: false },
|
||||||
|
{ key: 'size', label: t('firmware.size'), _style: { width: '8%' }, filter: false },
|
||||||
|
{ key: 'revision', label: t('firmware.revision'), _style: { width: '60%' } },
|
||||||
|
{ key: 'show_details', label: '', _style: { width: '15%' }, filter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilter('');
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal show={show} onClose={toggle} size="xl">
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">#{device?.serialNumber}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody>
|
||||||
|
{show ? (
|
||||||
|
<div>
|
||||||
|
<CRow>
|
||||||
|
<CCol sm="2" className="pt-2">
|
||||||
|
{t('firmware.installed_firmware')}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="pt-2">{device.firmware}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="mt-3">
|
||||||
|
<CCol sm="2" className="pt-2">
|
||||||
|
{t('factory_reset.redirector')}
|
||||||
|
</CCol>
|
||||||
|
<CCol className="pt-2">
|
||||||
|
<CSwitch
|
||||||
|
color="primary"
|
||||||
|
defaultChecked={keepRedirector}
|
||||||
|
onClick={toggleRedirector}
|
||||||
|
labelOn="Yes"
|
||||||
|
labelOff="No"
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-4">
|
||||||
|
<CCol sm="5">
|
||||||
|
<CInput
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
<CCol />
|
||||||
|
</CRow>
|
||||||
|
<CRow className="mb-4">
|
||||||
|
<CCol>
|
||||||
|
<div className="overflow-auto" style={{ height: '600px' }}>
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
items={firmwareVersions}
|
||||||
|
fields={fields}
|
||||||
|
loading={loading}
|
||||||
|
hover
|
||||||
|
tableFilterValue={filter}
|
||||||
|
border
|
||||||
|
scopedSlots={{
|
||||||
|
imageDate: (item) => <td>{prettyDate(item.imageDate)}</td>,
|
||||||
|
size: (item) => <td>{cleanBytesString(item.size)}</td>,
|
||||||
|
show_details: (item) => (
|
||||||
|
<td className="text-center">
|
||||||
|
<LoadingButton
|
||||||
|
label={t('firmware.upgrade')}
|
||||||
|
isLoadingLabel={t('firmware.upgrading')}
|
||||||
|
isLoading={false}
|
||||||
|
action={() => upgradeToVersion(item.uri)}
|
||||||
|
block={false}
|
||||||
|
disabled={upgradeStatus.loading}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceFirmwareModal.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
device: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
firmwareVersions: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
upgradeToVersion: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
keepRedirector: PropTypes.bool.isRequired,
|
||||||
|
toggleRedirector: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceFirmwareModal);
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { DeviceFirmwareModal as Modal, useAuth, useToast } from 'ucentral-libs';
|
import { useAuth, useToast, useToggle } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
const DeviceFirmwareModal = ({
|
const DeviceFirmwareModal = ({
|
||||||
device,
|
device,
|
||||||
@@ -17,6 +18,7 @@ const DeviceFirmwareModal = ({
|
|||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [firmwareVersions, setFirmwareVersions] = useState([]);
|
const [firmwareVersions, setFirmwareVersions] = useState([]);
|
||||||
|
const [keepRedirector, toggleKeepRedirector, setKeepRedirector] = useToggle(true);
|
||||||
|
|
||||||
const getPartialFirmware = async (offset) => {
|
const getPartialFirmware = async (offset) => {
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -78,6 +80,7 @@ const DeviceFirmwareModal = ({
|
|||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
serialNumber: device.serialNumber,
|
serialNumber: device.serialNumber,
|
||||||
|
keepRedirector,
|
||||||
when: 0,
|
when: 0,
|
||||||
uri,
|
uri,
|
||||||
};
|
};
|
||||||
@@ -108,6 +111,7 @@ const DeviceFirmwareModal = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show && device.compatible) getFirmwareList();
|
if (show && device.compatible) getFirmwareList();
|
||||||
|
if (show) setKeepRedirector(true);
|
||||||
}, [device, show]);
|
}, [device, show]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -120,6 +124,8 @@ const DeviceFirmwareModal = ({
|
|||||||
upgradeToVersion={upgradeToVersion}
|
upgradeToVersion={upgradeToVersion}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
upgradeStatus={upgradeStatus}
|
upgradeStatus={upgradeStatus}
|
||||||
|
keepRedirector={keepRedirector}
|
||||||
|
toggleRedirector={toggleKeepRedirector}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
/* eslint-disable-rule prefer-destructuring */
|
/* eslint-disable-rule prefer-destructuring */
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
CWidgetDropdown,
|
CCardBody,
|
||||||
CButton,
|
CButton,
|
||||||
CDataTable,
|
CDataTable,
|
||||||
CCard,
|
CCardHeader,
|
||||||
CRow,
|
|
||||||
CCol,
|
|
||||||
CProgress,
|
|
||||||
CPopover,
|
CPopover,
|
||||||
|
CCard,
|
||||||
|
CFormText,
|
||||||
|
CBadge,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilTrash } from '@coreui/icons';
|
import { cilSync, cilTrash } from '@coreui/icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DatePicker from 'react-widgets/DatePicker';
|
import DatePicker from 'react-widgets/DatePicker';
|
||||||
import { dateToUnix } from 'utils/helper';
|
import { dateToUnix } from 'utils/helper';
|
||||||
@@ -27,7 +27,9 @@ const DeviceHealth = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [healthChecks, setHealthChecks] = useState([]);
|
const [healthChecks, setHealthChecks] = useState([]);
|
||||||
const [start, setStart] = useState('');
|
const [start, setStart] = useState('');
|
||||||
|
const [startError, setStartError] = useState(false);
|
||||||
const [end, setEnd] = useState('');
|
const [end, setEnd] = useState('');
|
||||||
|
const [endError, setEndError] = useState(false);
|
||||||
const [logLimit, setLogLimit] = useState(25);
|
const [logLimit, setLogLimit] = useState(25);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const [showLoadingMore, setShowLoadingMore] = useState(true);
|
const [showLoadingMore, setShowLoadingMore] = useState(true);
|
||||||
@@ -40,13 +42,26 @@ const DeviceHealth = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const modifyStart = (value) => {
|
const modifyStart = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setStartError(false);
|
||||||
setStart(value);
|
setStart(value);
|
||||||
|
} catch (e) {
|
||||||
|
setStart('');
|
||||||
|
setStartError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyEnd = (value) => {
|
const modifyEnd = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setEndError(false);
|
||||||
setEnd(value);
|
setEnd(value);
|
||||||
|
} catch (e) {
|
||||||
|
setEnd('');
|
||||||
|
setEndError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showMoreLogs = () => {
|
const showMoreLogs = () => {
|
||||||
setLogLimit(logLimit + 50);
|
setLogLimit(logLimit + 50);
|
||||||
};
|
};
|
||||||
@@ -128,14 +143,14 @@ const DeviceHealth = () => {
|
|||||||
const tempSanityLevel = sortedHealthchecks[healthChecks.length - 1].sanity;
|
const tempSanityLevel = sortedHealthchecks[healthChecks.length - 1].sanity;
|
||||||
setSanityLevel(tempSanityLevel);
|
setSanityLevel(tempSanityLevel);
|
||||||
if (tempSanityLevel === 100) {
|
if (tempSanityLevel === 100) {
|
||||||
setBarColor('gradient-success');
|
setBarColor('success');
|
||||||
} else if (tempSanityLevel >= 90) {
|
} else if (tempSanityLevel >= 90) {
|
||||||
setBarColor('gradient-warning');
|
setBarColor('warning');
|
||||||
} else {
|
} else {
|
||||||
setBarColor('gradient-danger');
|
setBarColor('danger');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setBarColor('gradient-dark');
|
setBarColor('dark');
|
||||||
}
|
}
|
||||||
}, [healthChecks]);
|
}, [healthChecks]);
|
||||||
|
|
||||||
@@ -156,30 +171,61 @@ const DeviceHealth = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CWidgetDropdown
|
<CCard className="m-0">
|
||||||
className="m-0"
|
<CCardHeader className="dark-header">
|
||||||
header={t('health.title')}
|
<div className="float-left align-middle pt-1">
|
||||||
text={sanityLevel ? `${sanityLevel}%` : t('common.unknown')}
|
<h4>
|
||||||
value={sanityLevel ?? 100}
|
<CBadge color={barColor} className="my-0">
|
||||||
color={barColor}
|
{sanityLevel ? `${sanityLevel}%` : `${t('common.unknown')} Sanity Level`}
|
||||||
inverse="true"
|
</CBadge>
|
||||||
footerSlot={
|
</h4>
|
||||||
<div className="pb-1 px-3">
|
</div>
|
||||||
<CProgress className="mb-3" color="white" value={sanityLevel ?? 0} />
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
<CRow className="mb-3">
|
<div className="pl-2">
|
||||||
<CCol>
|
<CPopover content={t('common.refresh')}>
|
||||||
{t('common.from')}
|
<CButton
|
||||||
:
|
size="sm"
|
||||||
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
|
color="info"
|
||||||
</CCol>
|
onClick={getDeviceHealth}
|
||||||
<CCol>
|
disabled={startError || endError}
|
||||||
{t('common.to')}
|
>
|
||||||
:
|
<CIcon content={cilSync} />
|
||||||
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
|
</CButton>
|
||||||
</CCol>
|
</CPopover>
|
||||||
</CRow>
|
</div>
|
||||||
<CCard className="p-0">
|
<div className="pl-2">
|
||||||
<div className="overflow-auto" style={{ height: '200px' }}>
|
<DatePicker
|
||||||
|
includeTime
|
||||||
|
onChange={(date) => modifyEnd(date)}
|
||||||
|
value={end ? new Date(end) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!endError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
To:
|
||||||
|
<div className="pl-2">
|
||||||
|
<DatePicker
|
||||||
|
includeTime
|
||||||
|
onChange={(date) => modifyStart(date)}
|
||||||
|
value={start ? new Date(start) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!startError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
From:
|
||||||
|
<div className="px-2">
|
||||||
|
<CPopover content={t('common.delete')}>
|
||||||
|
<CButton onClick={toggleDeleteModal} size="sm" color="danger">
|
||||||
|
<CIcon name="cil-trash" content={cilTrash} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
|
||||||
<CDataTable
|
<CDataTable
|
||||||
addTableClasses="ignore-overflow table-sm"
|
addTableClasses="ignore-overflow table-sm"
|
||||||
border
|
border
|
||||||
@@ -214,25 +260,15 @@ const DeviceHealth = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</CCard>
|
|
||||||
<DeleteLogModal
|
<DeleteLogModal
|
||||||
serialNumber={deviceSerialNumber}
|
serialNumber={deviceSerialNumber}
|
||||||
object="healthchecks"
|
object="logs"
|
||||||
show={showDeleteModal}
|
show={showDeleteModal}
|
||||||
toggle={toggleDeleteModal}
|
toggle={toggleDeleteModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
</CCardBody>
|
||||||
>
|
</CCard>
|
||||||
<div className="text-right float-right">
|
|
||||||
<CPopover content={t('common.delete')}>
|
|
||||||
<CButton onClick={toggleDeleteModal} size="sm">
|
|
||||||
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
</div>
|
|
||||||
</CWidgetDropdown>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
456
src/components/DeviceListTable/Table/index.js
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactPaginate from 'react-paginate';
|
||||||
|
import {
|
||||||
|
CCardBody,
|
||||||
|
CDataTable,
|
||||||
|
CButton,
|
||||||
|
CLink,
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CPopover,
|
||||||
|
CSelect,
|
||||||
|
CButtonClose,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import {
|
||||||
|
cilSync,
|
||||||
|
cilArrowCircleTop,
|
||||||
|
cilCheckCircle,
|
||||||
|
cilTerminal,
|
||||||
|
cilTrash,
|
||||||
|
cilSearch,
|
||||||
|
} from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import { cleanBytesString } from 'utils/helper';
|
||||||
|
import { DeviceBadge, LoadingButton } from 'ucentral-libs';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const DeviceListTable = ({
|
||||||
|
currentPage,
|
||||||
|
devices,
|
||||||
|
searchBar,
|
||||||
|
devicesPerPage,
|
||||||
|
loading,
|
||||||
|
updateDevicesPerPage,
|
||||||
|
pageCount,
|
||||||
|
updatePage,
|
||||||
|
refreshDevice,
|
||||||
|
t,
|
||||||
|
toggleFirmwareModal,
|
||||||
|
toggleHistoryModal,
|
||||||
|
upgradeToLatest,
|
||||||
|
upgradeStatus,
|
||||||
|
deviceIcons,
|
||||||
|
connectRtty,
|
||||||
|
deleteDevice,
|
||||||
|
deleteStatus,
|
||||||
|
}) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '1%' } },
|
||||||
|
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '6%' } },
|
||||||
|
{ key: 'firmware', label: t('firmware.revision') },
|
||||||
|
{ key: 'firmware_button', label: '', filter: false, _style: { width: '1%' } },
|
||||||
|
{ key: 'compatible', label: t('common.type'), filter: false, _style: { width: '13%' } },
|
||||||
|
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '14%' } },
|
||||||
|
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '14%' } },
|
||||||
|
{ key: 'ipAddress', label: t('IP'), _style: { width: '10%' } },
|
||||||
|
{ key: 'twoG', label: t('2G'), _style: { width: '10%' } },
|
||||||
|
{ key: 'fiveG', label: t('5G'), _style: { width: '10%' } },
|
||||||
|
{ key: 'actions', label: t('actions.actions'), _style: { width: '10%' } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const hideTooltips = () => ReactTooltip.hide();
|
||||||
|
|
||||||
|
const escFunction = (event) => {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
hideTooltips();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShortRevision = (revision) => {
|
||||||
|
if (revision.includes(' / ')) {
|
||||||
|
return revision.split(' / ')[1];
|
||||||
|
}
|
||||||
|
return revision;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('keydown', escFunction, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', escFunction, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getFirmwareButton = (latest, device) => {
|
||||||
|
const tooltipId = createUuid();
|
||||||
|
let text = t('firmware.unknown_firmware_status');
|
||||||
|
let upgradeText = t('firmware.upgrade_to_latest');
|
||||||
|
let icon = <CIcon name="cil-arrow-circle-top" content={cilArrowCircleTop} />;
|
||||||
|
let color = 'secondary';
|
||||||
|
if (latest !== undefined) {
|
||||||
|
text = t('firmware.newer_firmware_available');
|
||||||
|
color = 'warning';
|
||||||
|
|
||||||
|
if (latest) {
|
||||||
|
icon = <CIcon name="cil-check-circle" content={cilCheckCircle} />;
|
||||||
|
text = t('firmware.latest_version_installed');
|
||||||
|
upgradeText = t('firmware.reinstall_latest');
|
||||||
|
color = 'success';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CButton size="sm" color={color} data-tip data-for={tooltipId} data-event="click">
|
||||||
|
{icon}
|
||||||
|
</CButton>
|
||||||
|
<ReactTooltip
|
||||||
|
id={tooltipId}
|
||||||
|
place="top"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
clickable
|
||||||
|
className={[styles.firmwareTooltip, 'tooltipLeft'].join(' ')}
|
||||||
|
border
|
||||||
|
borderColor="#321fdb"
|
||||||
|
arrowColor="white"
|
||||||
|
overridePosition={({ left, top }) => {
|
||||||
|
const element = document.getElementById(tooltipId);
|
||||||
|
const tooltipWidth = element ? element.offsetWidth : 0;
|
||||||
|
const newLeft = left + tooltipWidth * 0.25;
|
||||||
|
return { top, left: newLeft };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CCardHeader color="primary" className={styles.tooltipHeader}>
|
||||||
|
{text}
|
||||||
|
<CButtonClose
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<LoadingButton
|
||||||
|
variant="outline"
|
||||||
|
label={upgradeText}
|
||||||
|
isLoadingLabel={t('firmware.upgrading')}
|
||||||
|
isLoading={upgradeStatus.loading}
|
||||||
|
action={() => upgradeToLatest(device)}
|
||||||
|
block
|
||||||
|
disabled={
|
||||||
|
upgradeStatus.loading && upgradeStatus.serialNumber === device.serialNumber
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CButton
|
||||||
|
block
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
toggleFirmwareModal(device);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('firmware.choose_custom')}
|
||||||
|
</CButton>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CButton
|
||||||
|
block
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
toggleHistoryModal(device);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('firmware.history_title')}
|
||||||
|
</CButton>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
</ReactTooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteButton = (serialNumber) => {
|
||||||
|
const tooltipId = createUuid();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CPopover content={t('common.delete_device')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
data-tip
|
||||||
|
data-for={tooltipId}
|
||||||
|
data-event="click"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-trash" content={cilTrash} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<ReactTooltip
|
||||||
|
id={tooltipId}
|
||||||
|
place="top"
|
||||||
|
effect="solid"
|
||||||
|
globalEventOff="click"
|
||||||
|
clickable
|
||||||
|
className={[styles.deleteTooltip, 'tooltipRight'].join(' ')}
|
||||||
|
border
|
||||||
|
borderColor="#321fdb"
|
||||||
|
arrowColor="white"
|
||||||
|
overridePosition={({ left, top }) => {
|
||||||
|
const element = document.getElementById(tooltipId);
|
||||||
|
const tooltipWidth = element ? element.offsetWidth : 0;
|
||||||
|
const newLeft = left - tooltipWidth * 0.25;
|
||||||
|
return { top, left: newLeft };
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CCardHeader color="primary" className={styles.tooltipHeader}>
|
||||||
|
{t('common.device_delete', { serialNumber })}
|
||||||
|
<CButtonClose
|
||||||
|
className="p-0 mb-1"
|
||||||
|
style={{ color: 'white' }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.target.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<LoadingButton
|
||||||
|
data-toggle="dropdown"
|
||||||
|
variant="outline"
|
||||||
|
color="danger"
|
||||||
|
label={t('common.confirm')}
|
||||||
|
isLoadingLabel={t('user.deleting')}
|
||||||
|
isLoading={deleteStatus.loading}
|
||||||
|
action={(e) => {
|
||||||
|
e.target.parentNode.parentNode.parentNode.parentNode.classList.remove('show');
|
||||||
|
hideTooltips();
|
||||||
|
deleteDevice(serialNumber);
|
||||||
|
}}
|
||||||
|
block
|
||||||
|
disabled={deleteStatus.loading}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
</ReactTooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CCard className="m-0 p-0">
|
||||||
|
<CCardHeader className="p-0">
|
||||||
|
<div className="float-left" style={{ width: '400px' }}>
|
||||||
|
{searchBar}
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-0">
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
items={devices ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
scopedSlots={{
|
||||||
|
deviceType: (item) => (
|
||||||
|
<td className="align-middle text-center">
|
||||||
|
<DeviceBadge t={t} device={item} deviceIcons={deviceIcons} />
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
serialNumber: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
{item.serialNumber}
|
||||||
|
</CLink>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
firmware: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover
|
||||||
|
content={item.firmware ? item.firmware : t('common.na')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
|
||||||
|
{getShortRevision(item.firmware)}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
firmware_button: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
{item.firmwareInfo
|
||||||
|
? getFirmwareButton(item.firmwareInfo.latest, item)
|
||||||
|
: getFirmwareButton(undefined, item)}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
compatible: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover
|
||||||
|
content={item.compatible ? item.compatible : t('common.na')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div style={{ width: 'calc(10vw)' }} className="text-truncate align-middle">
|
||||||
|
{item.compatible}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
txBytes: (item) => <td className="align-middle">{cleanBytesString(item.txBytes)}</td>,
|
||||||
|
rxBytes: (item) => <td className="align-middle">{cleanBytesString(item.rxBytes)}</td>,
|
||||||
|
ipAddress: (item) => (
|
||||||
|
<td className="align-middle">
|
||||||
|
<CPopover
|
||||||
|
content={item.ipAddress ? item.ipAddress : t('common.na')}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<div style={{ width: 'calc(8vw)' }} className="text-truncate align-middle">
|
||||||
|
{item.ipAddress}
|
||||||
|
</div>
|
||||||
|
</CPopover>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
twoG: (item) => <td className="align-middle">{item.associations_2G ?? 0}</td>,
|
||||||
|
fiveG: (item) => <td className="align-middle">{item.associations_5G ?? 0}</td>,
|
||||||
|
actions: (item) => (
|
||||||
|
<td className="text-center align-middle">
|
||||||
|
<div role="group" className="justify-content-center" style={{ width: '190px' }}>
|
||||||
|
<CPopover content={t('actions.connect')}>
|
||||||
|
<CButton
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => connectRtty(item.serialNumber)}
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-terminal" content={cilTerminal} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
{deleteButton(item.serialNumber)}
|
||||||
|
<CPopover content={t('configuration.details')}>
|
||||||
|
<CLink
|
||||||
|
className="c-subheader-nav-link"
|
||||||
|
aria-current="page"
|
||||||
|
to={() => `/devices/${item.serialNumber}`}
|
||||||
|
>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-search" content={cilSearch} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CLink>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.refresh_device')}>
|
||||||
|
<CButton
|
||||||
|
onClick={() => refreshDevice(item.serialNumber)}
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
shape="square"
|
||||||
|
size="sm"
|
||||||
|
className="mx-1 d-inline"
|
||||||
|
style={{ width: '33px', height: '30px' }}
|
||||||
|
>
|
||||||
|
<CIcon name="cil-sync" content={cilSync} size="sm" />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row pl-3">
|
||||||
|
<div className="pr-3">
|
||||||
|
<ReactPaginate
|
||||||
|
previousLabel="← Previous"
|
||||||
|
nextLabel="Next →"
|
||||||
|
pageCount={pageCount}
|
||||||
|
onPageChange={updatePage}
|
||||||
|
forcePage={Number(currentPage)}
|
||||||
|
breakClassName="page-item"
|
||||||
|
breakLinkClassName="page-link"
|
||||||
|
containerClassName="pagination"
|
||||||
|
pageClassName="page-item"
|
||||||
|
pageLinkClassName="page-link"
|
||||||
|
previousClassName="page-item"
|
||||||
|
previousLinkClassName="page-link"
|
||||||
|
nextClassName="page-item"
|
||||||
|
nextLinkClassName="page-link"
|
||||||
|
activeClassName="active"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="pr-2 mt-1">{t('common.items_per_page')}</p>
|
||||||
|
<div style={{ width: '100px' }} className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
defaultValue={devicesPerPage}
|
||||||
|
onChange={(e) => updateDevicesPerPage(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
</CSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceListTable.propTypes = {
|
||||||
|
currentPage: PropTypes.oneOf(['string', 'number']),
|
||||||
|
devices: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
searchBar: PropTypes.node.isRequired,
|
||||||
|
updateDevicesPerPage: PropTypes.func.isRequired,
|
||||||
|
pageCount: PropTypes.number.isRequired,
|
||||||
|
updatePage: PropTypes.func.isRequired,
|
||||||
|
devicesPerPage: PropTypes.string.isRequired,
|
||||||
|
refreshDevice: PropTypes.func.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
toggleFirmwareModal: PropTypes.func.isRequired,
|
||||||
|
toggleHistoryModal: PropTypes.func.isRequired,
|
||||||
|
upgradeToLatest: PropTypes.func.isRequired,
|
||||||
|
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
deviceIcons: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
connectRtty: PropTypes.func.isRequired,
|
||||||
|
deleteDevice: PropTypes.func.isRequired,
|
||||||
|
deleteStatus: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceListTable.defaultProps = {
|
||||||
|
currentPage: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceListTable);
|
||||||
30
src/components/DeviceListTable/Table/index.module.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.firmwareTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteTooltip {
|
||||||
|
opacity: 1 !important;
|
||||||
|
padding: 0px 0px 0px 0px !important;
|
||||||
|
border-radius: 1rem !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
border-color: #321fdb !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipHeader {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
border-top-left-radius: 1rem !important;
|
||||||
|
border-top-right-radius: 1rem !important;
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import { getItem, setItem } from 'utils/localStorageHelper';
|
|||||||
import DeviceSearchBar from 'components/DeviceSearchBar';
|
import DeviceSearchBar from 'components/DeviceSearchBar';
|
||||||
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
|
||||||
import FirmwareHistoryModal from 'components/FirmwareHistoryModal';
|
import FirmwareHistoryModal from 'components/FirmwareHistoryModal';
|
||||||
import { DeviceListTable, useAuth, useToast } from 'ucentral-libs';
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
|
import Table from './Table';
|
||||||
import meshIcon from '../../assets/icons/Mesh.png';
|
import meshIcon from '../../assets/icons/Mesh.png';
|
||||||
import apIcon from '../../assets/icons/AP.png';
|
import apIcon from '../../assets/icons/AP.png';
|
||||||
import internetSwitch from '../../assets/icons/Switch.png';
|
import internetSwitch from '../../assets/icons/Switch.png';
|
||||||
@@ -377,7 +378,7 @@ const DeviceList = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DeviceListTable
|
<Table
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
t={t}
|
t={t}
|
||||||
searchBar={<DeviceSearchBar />}
|
searchBar={<DeviceSearchBar />}
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
/* eslint-disable-rule prefer-destructuring */
|
/* eslint-disable-rule prefer-destructuring */
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
CWidgetDropdown,
|
CCardHeader,
|
||||||
CRow,
|
CCardBody,
|
||||||
CCol,
|
|
||||||
CCollapse,
|
CCollapse,
|
||||||
CButton,
|
CButton,
|
||||||
CDataTable,
|
CDataTable,
|
||||||
CCard,
|
CCard,
|
||||||
CCardBody,
|
|
||||||
CPopover,
|
CPopover,
|
||||||
|
CFormText,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilTrash } from '@coreui/icons';
|
import { cilSync, cilTrash } from '@coreui/icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DatePicker from 'react-widgets/DatePicker';
|
import DatePicker from 'react-widgets/DatePicker';
|
||||||
import { dateToUnix } from 'utils/helper';
|
import { dateToUnix } from 'utils/helper';
|
||||||
@@ -29,7 +28,9 @@ const DeviceLogs = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [start, setStart] = useState('');
|
const [start, setStart] = useState('');
|
||||||
|
const [startError, setStartError] = useState(false);
|
||||||
const [end, setEnd] = useState('');
|
const [end, setEnd] = useState('');
|
||||||
|
const [endError, setEndError] = useState(false);
|
||||||
const [logLimit, setLogLimit] = useState(25);
|
const [logLimit, setLogLimit] = useState(25);
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
const [showLoadingMore, setShowLoadingMore] = useState(true);
|
const [showLoadingMore, setShowLoadingMore] = useState(true);
|
||||||
@@ -40,11 +41,25 @@ const DeviceLogs = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const modifyStart = (value) => {
|
const modifyStart = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setStartError(false);
|
||||||
setStart(value);
|
setStart(value);
|
||||||
|
} catch (e) {
|
||||||
|
setStart('');
|
||||||
|
setStartError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const modifyEnd = (value) => {
|
const modifyEnd = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setEndError(false);
|
||||||
setEnd(value);
|
setEnd(value);
|
||||||
|
} catch (e) {
|
||||||
|
setEnd('');
|
||||||
|
setEndError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showMoreLogs = () => {
|
const showMoreLogs = () => {
|
||||||
@@ -167,25 +182,49 @@ const DeviceLogs = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CWidgetDropdown
|
<CCard className="m-0">
|
||||||
className="m-0"
|
<CCardHeader className="dark-header">
|
||||||
inverse="true"
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
color="gradient-info"
|
<div className="pl-2">
|
||||||
header={t('device_logs.title')}
|
<CPopover content={t('common.refresh')}>
|
||||||
footerSlot={
|
<CButton size="sm" color="info" onClick={getLogs} disabled={startError || endError}>
|
||||||
<div className="pb-1 px-3">
|
<CIcon content={cilSync} />
|
||||||
<CRow className="mb-3">
|
</CButton>
|
||||||
<CCol>
|
</CPopover>
|
||||||
{t('common.from')}
|
</div>
|
||||||
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
|
<div className="pl-2">
|
||||||
</CCol>
|
<DatePicker
|
||||||
<CCol>
|
includeTime
|
||||||
{t('common.to')}
|
onChange={(date) => modifyEnd(date)}
|
||||||
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
|
value={end ? new Date(end) : undefined}
|
||||||
</CCol>
|
/>
|
||||||
</CRow>
|
<CFormText color="danger" hidden={!endError}>
|
||||||
<CCard>
|
{t('common.invalid_date_explanation')}
|
||||||
<div className="overflow-auto" style={{ height: '250px' }}>
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
To:
|
||||||
|
<div className="pl-2">
|
||||||
|
<DatePicker
|
||||||
|
includeTime
|
||||||
|
onChange={(date) => modifyStart(date)}
|
||||||
|
value={start ? new Date(start) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!startError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
From:
|
||||||
|
<div className="px-2">
|
||||||
|
<CPopover content={t('common.delete')}>
|
||||||
|
<CButton onClick={toggleDeleteModal} size="sm" color="danger">
|
||||||
|
<CIcon name="cil-trash" content={cilTrash} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<div className="overflow-auto" style={{ height: 'calc(100vh - 620px)' }}>
|
||||||
<CDataTable
|
<CDataTable
|
||||||
addTableClasses="ignore-overflow table-sm"
|
addTableClasses="ignore-overflow table-sm"
|
||||||
border
|
border
|
||||||
@@ -214,7 +253,7 @@ const DeviceLogs = () => {
|
|||||||
toggleDetails(index);
|
toggleDetails(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CIcon name="cilList" size="md" />
|
<CIcon name="cilList" />
|
||||||
</CButton>
|
</CButton>
|
||||||
</td>
|
</td>
|
||||||
),
|
),
|
||||||
@@ -240,18 +279,8 @@ const DeviceLogs = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</CCardBody>
|
||||||
</CCard>
|
</CCard>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="text-right float-right">
|
|
||||||
<CPopover content={t('common.delete')}>
|
|
||||||
<CButton onClick={toggleDeleteModal} size="sm">
|
|
||||||
<CIcon name="cil-trash" content={cilTrash} className="text-white" size="2xl" />
|
|
||||||
</CButton>
|
|
||||||
</CPopover>
|
|
||||||
</div>
|
|
||||||
</CWidgetDropdown>
|
|
||||||
<DeleteLogModal
|
<DeleteLogModal
|
||||||
serialNumber={deviceSerialNumber}
|
serialNumber={deviceSerialNumber}
|
||||||
object="logs"
|
object="logs"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
|
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
|
||||||
import { checkIfJson } from 'utils/helper';
|
import { checkIfJson } from 'utils/helper';
|
||||||
|
|
||||||
const DeviceSearchBar = () => {
|
const DeviceSearchBar = ({ action }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
@@ -14,7 +15,7 @@ const DeviceSearchBar = () => {
|
|||||||
|
|
||||||
const search = (value) => {
|
const search = (value) => {
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
if (value.length > 0 && value.match('^[a-fA-F0-9]+$')) {
|
if (value.length > 1 && value.match('^[a-fA-F0-9-*]+$')) {
|
||||||
setWaitingSearch('');
|
setWaitingSearch('');
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify({ command: 'serial_number_search', serial_prefix: value.toLowerCase() }),
|
JSON.stringify({ command: 'serial_number_search', serial_prefix: value.toLowerCase() }),
|
||||||
@@ -65,7 +66,15 @@ const DeviceSearchBar = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <SearchBar t={t} search={search} results={results} history={history} />;
|
return <SearchBar t={t} search={search} results={results} history={history} action={action} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceSearchBar.propTypes = {
|
||||||
|
action: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceSearchBar.defaultProps = {
|
||||||
|
action: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeviceSearchBar;
|
export default DeviceSearchBar;
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { DeviceStatusCard as Card, useDevice, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
|
|
||||||
const DeviceStatusCard = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { deviceSerialNumber } = useDevice();
|
|
||||||
const { addToast } = useToast();
|
|
||||||
const [lastStats, setLastStats] = useState(null);
|
|
||||||
const [status, setStatus] = useState(null);
|
|
||||||
const [deviceConfig, setDeviceConfig] = useState(null);
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const getDevice = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
setDeviceConfig(response.data);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('device.error_fetching_device', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getData = () => {
|
|
||||||
setLoading(true);
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const lastStatsRequest = axiosInstance.get(
|
|
||||||
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(
|
|
||||||
deviceSerialNumber,
|
|
||||||
)}/statistics?lastOnly=true`,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
const statusRequest = axiosInstance.get(
|
|
||||||
`${endpoints.owgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}/status`,
|
|
||||||
options,
|
|
||||||
);
|
|
||||||
|
|
||||||
Promise.all([lastStatsRequest, statusRequest])
|
|
||||||
.then(([newStats, newStatus]) => {
|
|
||||||
setLastStats(newStats.data);
|
|
||||||
setStatus(newStatus.data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setError(true);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const refresh = () => {
|
|
||||||
getData();
|
|
||||||
getDevice();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setError(false);
|
|
||||||
if (deviceSerialNumber) {
|
|
||||||
getDevice();
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
}, [deviceSerialNumber]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
t={t}
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
deviceSerialNumber={deviceSerialNumber}
|
|
||||||
getData={refresh}
|
|
||||||
deviceConfig={deviceConfig}
|
|
||||||
status={status}
|
|
||||||
lastStats={lastStats}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeviceStatusCard;
|
|
||||||
154
src/components/EditBlacklistModal/index.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CCol,
|
||||||
|
CLabel,
|
||||||
|
CTextarea,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { useAuth, useToast } from 'ucentral-libs';
|
||||||
|
import { cilSave, cilX } from '@coreui/icons';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
|
||||||
|
const EditBlacklistModal = ({ show, toggle, serialNumber, refresh }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { endpoints, currentToken } = useAuth();
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
|
||||||
|
const getBlacklistInfo = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setReason(response.data.reason);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_fetching_devices', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
reason,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.put(`${endpoints.owgw}/api/v1/blacklist/${serialNumber}`, parameters, { headers })
|
||||||
|
.then(() => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('device.success_edit_blacklist'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggle();
|
||||||
|
if (refresh) refresh();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('device.error_edit_blacklist', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) getBlacklistInfo();
|
||||||
|
}, [show, serialNumber]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('device.edit_blacklist')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.add')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={save}
|
||||||
|
disabled={loading || reason === ''}
|
||||||
|
>
|
||||||
|
<CIcon content={cilSave} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody>
|
||||||
|
<CRow>
|
||||||
|
<CLabel col sm="3">
|
||||||
|
{t('common.reason')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="9" className="pt-2">
|
||||||
|
<CTextarea
|
||||||
|
name="reason"
|
||||||
|
id="reason"
|
||||||
|
rows="3"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
/>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditBlacklistModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
serialNumber: PropTypes.string,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditBlacklistModal.defaultProps = {
|
||||||
|
serialNumber: '',
|
||||||
|
refresh: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditBlacklistModal;
|
||||||
163
src/components/EditConfigurationModal/Form.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Select from 'react-select';
|
||||||
|
import {
|
||||||
|
CForm,
|
||||||
|
CInput,
|
||||||
|
CLabel,
|
||||||
|
CCol,
|
||||||
|
CFormGroup,
|
||||||
|
CInvalidFeedback,
|
||||||
|
CFormText,
|
||||||
|
CRow,
|
||||||
|
CTextarea,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CopyToClipboardButton } from 'ucentral-libs';
|
||||||
|
|
||||||
|
const EditDefaultConfigurationForm = ({
|
||||||
|
t,
|
||||||
|
disable,
|
||||||
|
fields,
|
||||||
|
updateField,
|
||||||
|
updateFieldWithKey,
|
||||||
|
deviceTypes,
|
||||||
|
editing,
|
||||||
|
}) => {
|
||||||
|
const [typeOptions, setTypeOptions] = useState([]);
|
||||||
|
const [chosenTypes, setChosenTypes] = useState([]);
|
||||||
|
|
||||||
|
const parseOptions = () => {
|
||||||
|
const options = [{ value: '*', label: 'All' }];
|
||||||
|
const newOptions = deviceTypes.map((option) => ({
|
||||||
|
value: option,
|
||||||
|
label: option,
|
||||||
|
}));
|
||||||
|
options.push(...newOptions);
|
||||||
|
setTypeOptions(options);
|
||||||
|
setChosenTypes([]);
|
||||||
|
|
||||||
|
const newChosenTypes = fields.modelIds.value.map((dType) => ({
|
||||||
|
value: dType,
|
||||||
|
label: dType === '*' ? 'All' : dType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setChosenTypes(newChosenTypes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeOnChange = (chosenArray) => {
|
||||||
|
const allIndex = chosenArray.findIndex((el) => el.value === '*');
|
||||||
|
|
||||||
|
// If the All option was chosen before, we take it out of the array
|
||||||
|
if (allIndex === 0 && chosenTypes.length > 0) {
|
||||||
|
const newResults = chosenArray.slice(1);
|
||||||
|
setChosenTypes(newResults);
|
||||||
|
updateFieldWithKey('modelIds', {
|
||||||
|
value: newResults.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else if (allIndex > 0) {
|
||||||
|
setChosenTypes([{ value: '*', label: 'All' }]);
|
||||||
|
updateFieldWithKey('modelIds', { value: ['*'], error: false, notEmpty: true });
|
||||||
|
} else if (chosenArray.length > 0) {
|
||||||
|
setChosenTypes(chosenArray);
|
||||||
|
updateFieldWithKey('modelIds', {
|
||||||
|
value: chosenArray.map((el) => el.value),
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setChosenTypes([]);
|
||||||
|
updateFieldWithKey('modelIds', { value: [], error: false, notEmpty: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
parseOptions();
|
||||||
|
}, [deviceTypes, fields.name.value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CForm>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="name">
|
||||||
|
{t('user.name')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7" className="pt-2">
|
||||||
|
{fields.name.value}
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CFormGroup row className="pb-3">
|
||||||
|
<CLabel col htmlFor="description">
|
||||||
|
{t('user.description')}
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.description.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.description.error}
|
||||||
|
disabled={disable || !editing}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
<CInvalidFeedback>{t('common.required')}</CInvalidFeedback>
|
||||||
|
</CCol>
|
||||||
|
</CFormGroup>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CLabel col htmlFor="deviceTypes">
|
||||||
|
<div>{t('configuration.supported_device_types')}:</div>
|
||||||
|
</CLabel>
|
||||||
|
<CCol sm="7">
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
closeMenuOnSelect={false}
|
||||||
|
id="deviceTypes"
|
||||||
|
options={typeOptions}
|
||||||
|
onChange={typeOnChange}
|
||||||
|
value={chosenTypes}
|
||||||
|
className={`basic-multi-select ${fields.modelIds.error ? 'border-danger' : ''}`}
|
||||||
|
classNamePrefix="select"
|
||||||
|
isDisabled={disable || !editing}
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.modelIds.error} color="danger">
|
||||||
|
{t('configuration.need_device_type')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<div className="pb-3">
|
||||||
|
{t('configuration.title')}
|
||||||
|
<CopyToClipboardButton t={t} size="sm" content={fields.configuration.value} />
|
||||||
|
</div>
|
||||||
|
<CRow className="pb-3">
|
||||||
|
<CCol>
|
||||||
|
<CTextarea
|
||||||
|
style={{ overflowY: 'scroll', height: '500px' }}
|
||||||
|
id="configuration"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={fields.configuration.value}
|
||||||
|
onChange={updateField}
|
||||||
|
invalid={fields.configuration.error}
|
||||||
|
disabled={disable || !editing}
|
||||||
|
/>
|
||||||
|
<CFormText hidden={!fields.configuration.error} color="danger">
|
||||||
|
{t('common.required')}
|
||||||
|
</CFormText>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditDefaultConfigurationForm.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
disable: PropTypes.bool.isRequired,
|
||||||
|
fields: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
updateField: PropTypes.func.isRequired,
|
||||||
|
updateFieldWithKey: PropTypes.func.isRequired,
|
||||||
|
deviceTypes: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditDefaultConfigurationForm;
|
||||||
243
src/components/EditConfigurationModal/index.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CModal, CModalHeader, CModalTitle, CModalBody, CButton, CPopover } from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX, cilSave, cilPencil } from '@coreui/icons';
|
||||||
|
import { useToast, useFormFields, useAuth } from 'ucentral-libs';
|
||||||
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { checkIfJson } from 'utils/helper';
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const initialForm = {
|
||||||
|
name: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
},
|
||||||
|
modelIds: {
|
||||||
|
value: [],
|
||||||
|
error: false,
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
configuration: {
|
||||||
|
value: '',
|
||||||
|
error: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditConfigurationModal = ({ show, toggle, refresh, configId }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { addToast } = useToast();
|
||||||
|
const { currentToken, endpoints } = useAuth();
|
||||||
|
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialForm);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [deviceTypes, setDeviceTypes] = useState([]);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
const getConfig = () => {
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/default_configuration/${configId}`, options)
|
||||||
|
.then((response) => {
|
||||||
|
const newConfig = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(response.data)) {
|
||||||
|
if (key in initialForm) {
|
||||||
|
newConfig[key] = {
|
||||||
|
...initialForm[key],
|
||||||
|
value: response.data[key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newConfig.configuration.value = JSON.stringify(response.data.configuration, null, 2);
|
||||||
|
setFormFields(newConfig);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_fetching_config', {
|
||||||
|
error: e.response?.data?.ErrorDescription,
|
||||||
|
}),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
toggle();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeviceTypes = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(`${endpoints.owfms}/api/v1/firmwares?deviceSet=true`, {
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
setDeviceTypes([...response.data.deviceTypes]);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = () => {
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
for (const [key, field] of Object.entries(fields)) {
|
||||||
|
if (field.required && field.value === '') {
|
||||||
|
updateField(key, { error: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (field.notEmpty && field.value.length === 0) {
|
||||||
|
updateField(key, { error: true, notEmpty: true });
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkIfJson(fields.configuration.value)) {
|
||||||
|
updateField('configuration', { error: true });
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (validation()) {
|
||||||
|
setLoading(true);
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
name: fields.name.value,
|
||||||
|
description: fields.description.value,
|
||||||
|
modelIds: fields.modelIds.value,
|
||||||
|
configuration: fields.configuration.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.put(`${endpoints.owgw}/api/v1/default_configuration/${configId}`, parameters, options)
|
||||||
|
.then(() => {
|
||||||
|
if (refresh !== null) refresh();
|
||||||
|
toggle();
|
||||||
|
addToast({
|
||||||
|
title: t('common.success'),
|
||||||
|
body: t('configuration.success_update'),
|
||||||
|
color: 'success',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
addToast({
|
||||||
|
title: t('common.error'),
|
||||||
|
body: t('configuration.error_update', { error: e.response?.data?.ErrorDescription }),
|
||||||
|
color: 'danger',
|
||||||
|
autohide: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleEditing = () => {
|
||||||
|
if (editing) getConfig();
|
||||||
|
setEditing(!editing);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
setEditing(false);
|
||||||
|
getConfig();
|
||||||
|
getDeviceTypes();
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CModal className="text-dark" size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('configuration.edit_configuration')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.save')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={save}
|
||||||
|
disabled={!editing}
|
||||||
|
>
|
||||||
|
<CIcon content={cilSave} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.edit')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={toggleEditing}
|
||||||
|
disabled={editing}
|
||||||
|
>
|
||||||
|
<CIcon content={cilPencil} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody className="px-5">
|
||||||
|
<Form
|
||||||
|
t={t}
|
||||||
|
disable={loading}
|
||||||
|
fields={fields}
|
||||||
|
editing={editing}
|
||||||
|
updateField={updateFieldWithId}
|
||||||
|
updateFieldWithKey={updateField}
|
||||||
|
deviceTypes={deviceTypes}
|
||||||
|
show={show}
|
||||||
|
/>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
EditConfigurationModal.propTypes = {
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
refresh: PropTypes.func,
|
||||||
|
configId: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
EditConfigurationModal.defaultProps = {
|
||||||
|
refresh: null,
|
||||||
|
configId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditConfigurationModal;
|
||||||
54
src/components/EditFirmwareModal/Form.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CCardBody, CCol, CInput, CRow } from '@coreui/react';
|
||||||
|
import { prettyDate, cleanBytesString } from 'utils/helper';
|
||||||
|
|
||||||
|
const FirmwareDetailsForm = ({ t, fields, updateFieldsWithId, editing }) => (
|
||||||
|
<CCardBody className="p-1">
|
||||||
|
<CRow>
|
||||||
|
<CCol sm="2">{t('firmware.release')}</CCol>
|
||||||
|
<CCol sm="4">{fields.release.value}</CCol>
|
||||||
|
<CCol sm="2">{t('common.created')}</CCol>
|
||||||
|
<CCol sm="4">{prettyDate(fields.created.value)}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-3">
|
||||||
|
<CCol sm="2">{t('firmware.image_date')}</CCol>
|
||||||
|
<CCol sm="4">{prettyDate(fields.imageDate.value)}</CCol>
|
||||||
|
<CCol sm="2">{t('firmware.size')}</CCol>
|
||||||
|
<CCol sm="4">{cleanBytesString(fields.size.value)}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-3">
|
||||||
|
<CCol sm="2">{t('firmware.image')}</CCol>
|
||||||
|
<CCol sm="4">{fields.image.value}</CCol>
|
||||||
|
<CCol sm="2">{t('firmware.revision')}</CCol>
|
||||||
|
<CCol sm="4">{fields.revision.value}</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow className="my-3">
|
||||||
|
<CCol sm="2">URI</CCol>
|
||||||
|
<CCol sm="4">{fields.uri.value}</CCol>
|
||||||
|
<CCol sm="2" className="mt-2">
|
||||||
|
{t('user.description')}
|
||||||
|
</CCol>
|
||||||
|
<CCol sm="4">
|
||||||
|
{editing ? (
|
||||||
|
<CInput
|
||||||
|
id="description"
|
||||||
|
value={fields.description.value}
|
||||||
|
onChange={updateFieldsWithId}
|
||||||
|
maxLength="50"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="mt-2 mb-0">{fields.description.value}</p>
|
||||||
|
)}
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
</CCardBody>
|
||||||
|
);
|
||||||
|
|
||||||
|
FirmwareDetailsForm.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
fields: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
updateFieldsWithId: PropTypes.func.isRequired,
|
||||||
|
editing: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
export default FirmwareDetailsForm;
|
||||||
@@ -16,13 +16,8 @@ import {
|
|||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilPencil, cilSave, cilX } from '@coreui/icons';
|
import { cilPencil, cilSave, cilX } from '@coreui/icons';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import {
|
import { useFormFields, useAuth, useToast, DetailedNotesTable } from 'ucentral-libs';
|
||||||
useFormFields,
|
import Form from './Form';
|
||||||
useAuth,
|
|
||||||
useToast,
|
|
||||||
FirmwareDetailsForm,
|
|
||||||
DetailedNotesTable,
|
|
||||||
} from 'ucentral-libs';
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
created: {
|
created: {
|
||||||
@@ -237,12 +232,7 @@ const EditFirmwareModal = ({ show, toggle, firmwareId, refreshTable }) => {
|
|||||||
<CTabContent>
|
<CTabContent>
|
||||||
<CTabPane active={index === 0} className="pt-2">
|
<CTabPane active={index === 0} className="pt-2">
|
||||||
{index === 0 ? (
|
{index === 0 ? (
|
||||||
<FirmwareDetailsForm
|
<Form t={t} fields={firmware} updateFieldsWithId={updateWithId} editing={editing} />
|
||||||
t={t}
|
|
||||||
fields={firmware}
|
|
||||||
updateFieldsWithId={updateWithId}
|
|
||||||
editing={editing}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</CTabPane>
|
</CTabPane>
|
||||||
<CTabPane active={index === 1}>
|
<CTabPane active={index === 1}>
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { useUser, EditUserModal as Modal, useAuth, useToast } from 'ucentral-libs';
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
Id: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
changePassword: {
|
|
||||||
value: false,
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
currentPassword: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
value: '',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
userRole: {
|
|
||||||
value: 'accounting',
|
|
||||||
error: false,
|
|
||||||
editable: true,
|
|
||||||
},
|
|
||||||
notes: {
|
|
||||||
value: [],
|
|
||||||
editable: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditUserModal = ({ show, toggle, userId, getUsers, policies }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { addToast } = useToast();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [initialUser, setInitialUser] = useState({});
|
|
||||||
const [editing, setEditing] = useState(false);
|
|
||||||
const [user, updateWithId, updateWithKey, setUser] = useUser(initialState);
|
|
||||||
|
|
||||||
const getUser = () => {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(`${endpoints.owsec}/api/v1/user/${userId}`, options)
|
|
||||||
.then((response) => {
|
|
||||||
const newUser = {};
|
|
||||||
|
|
||||||
for (const key of Object.keys(response.data)) {
|
|
||||||
if (key in initialState && key !== 'currentPassword') {
|
|
||||||
newUser[key] = {
|
|
||||||
...initialState[key],
|
|
||||||
value: response.data[key],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setInitialUser({ ...initialState, ...newUser });
|
|
||||||
setUser({ ...initialState, ...newUser });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('common.error'),
|
|
||||||
body: t('user.error_retrieving'),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
toggle();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleEditing = () => {
|
|
||||||
if (editing) {
|
|
||||||
getUser();
|
|
||||||
}
|
|
||||||
setEditing(!editing);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateUser = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const parameters = {
|
|
||||||
id: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
let newData = false;
|
|
||||||
|
|
||||||
for (const key of Object.keys(user)) {
|
|
||||||
if (user[key].editable && user[key].value !== initialUser[key].value) {
|
|
||||||
if (key === 'currentPassword' && user[key].length < 8) {
|
|
||||||
updateWithKey('currentPassword', {
|
|
||||||
error: true,
|
|
||||||
});
|
|
||||||
newData = false;
|
|
||||||
break;
|
|
||||||
} else if (key === 'changePassword') {
|
|
||||||
parameters[key] = user[key].value === 'on';
|
|
||||||
newData = true;
|
|
||||||
} else {
|
|
||||||
parameters[key] = user[key].value;
|
|
||||||
newData = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNotes = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < user.notes.value.length; i += 1) {
|
|
||||||
if (user.notes.value[i].new) newNotes.push({ note: user.notes.value[i].note });
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters.notes = newNotes;
|
|
||||||
|
|
||||||
if (newData || newNotes.length > 0) {
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.put(`${endpoints.owsec}/api/v1/user/${userId}`, parameters, options)
|
|
||||||
.then(() => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_success_title'),
|
|
||||||
body: t('user.update_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getUsers();
|
|
||||||
toggle();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_failure_title'),
|
|
||||||
body: t('user.update_failure', { error: e.response?.data?.ErrorDescription }),
|
|
||||||
color: 'danger',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getUser();
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
addToast({
|
|
||||||
title: t('user.update_success_title'),
|
|
||||||
body: t('user.update_success'),
|
|
||||||
color: 'success',
|
|
||||||
autohide: true,
|
|
||||||
});
|
|
||||||
getUsers();
|
|
||||||
toggle();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addNote = (currentNote) => {
|
|
||||||
const newNotes = [...user.notes.value];
|
|
||||||
newNotes.unshift({
|
|
||||||
note: currentNote,
|
|
||||||
new: true,
|
|
||||||
created: new Date().getTime() / 1000,
|
|
||||||
createdBy: '',
|
|
||||||
});
|
|
||||||
updateWithKey('notes', { value: newNotes });
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userId) {
|
|
||||||
getUser();
|
|
||||||
}
|
|
||||||
}, [userId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (show) {
|
|
||||||
getUser();
|
|
||||||
setEditing(false);
|
|
||||||
}
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
t={t}
|
|
||||||
user={user}
|
|
||||||
updateUserWithId={updateWithId}
|
|
||||||
saveUser={updateUser}
|
|
||||||
loading={loading}
|
|
||||||
policies={policies}
|
|
||||||
show={show}
|
|
||||||
toggle={toggle}
|
|
||||||
editing={editing}
|
|
||||||
toggleEditing={toggleEditing}
|
|
||||||
addNote={addNote}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
EditUserModal.propTypes = {
|
|
||||||
userId: PropTypes.string.isRequired,
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
toggle: PropTypes.func.isRequired,
|
|
||||||
getUsers: PropTypes.func.isRequired,
|
|
||||||
policies: PropTypes.instanceOf(Object).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(EditUserModal);
|
|
||||||
45
src/components/EventQueueModal/Modal.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CModal,
|
||||||
|
CModalBody,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CSpinner,
|
||||||
|
CPopover,
|
||||||
|
CButton,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX } from '@coreui/icons';
|
||||||
|
|
||||||
|
const EventQueueModal = ({ t, show, toggle, loading, result }) => (
|
||||||
|
<CModal size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{t('commands.event_queue')}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody className="text-center">
|
||||||
|
{loading ? (
|
||||||
|
<CSpinner color="primary" size="lg" />
|
||||||
|
) : (
|
||||||
|
<pre className="ignore text-left">{JSON.stringify(result, null, 4)}</pre>
|
||||||
|
)}
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
);
|
||||||
|
|
||||||
|
EventQueueModal.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
show: PropTypes.bool.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
result: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventQueueModal;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { EventQueueModal as Modal, useAuth, useDevice, useToast } from 'ucentral-libs';
|
import { useAuth, useDevice, useToast } from 'ucentral-libs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
const EventQueueModal = ({ show, toggle }) => {
|
const EventQueueModal = ({ show, toggle }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
329
src/components/FirmwareDashboard/Dashboard/index.js
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardBody,
|
||||||
|
CCardHeader,
|
||||||
|
CCol,
|
||||||
|
CDataTable,
|
||||||
|
CPopover,
|
||||||
|
CRow,
|
||||||
|
CSpinner,
|
||||||
|
CWidgetIcon,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import { CChartBar, CChartHorizontalBar, CChartPie } from '@coreui/react-chartjs';
|
||||||
|
import { cilClock, cilHappy, cilMeh, cilFrown, cilBirthdayCake, cilInfo } from '@coreui/icons';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { FormattedDate } from 'ucentral-libs';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const getLatestColor = (percent = 0) => {
|
||||||
|
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
|
||||||
|
if (numberPercent >= 90) return 'success';
|
||||||
|
if (numberPercent > 60) return 'warning';
|
||||||
|
return 'danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLatestIcon = (percent = 0) => {
|
||||||
|
const numberPercent = percent ? Number(percent.replace('%', '')) : 0;
|
||||||
|
if (numberPercent >= 90) return <CIcon width={36} name="cil-happy" content={cilHappy} />;
|
||||||
|
if (numberPercent > 60) return <CIcon width={36} name="cil-meh" content={cilMeh} />;
|
||||||
|
return <CIcon width={36} name="cil-frown" content={cilFrown} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FirmwareDashboard = ({ t, data, loading }) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'endpoint', label: t('common.endpoint'), filter: false, sorter: false },
|
||||||
|
{ key: 'devices', label: t('common.devices') },
|
||||||
|
{ key: 'percent', label: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{ opacity: loading ? '20%' : '100%' }}>
|
||||||
|
<CRow className="mt-3">
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.last_dashboard_refresh')}
|
||||||
|
header={data.snapshot ? <FormattedDate date={data.snapshot} size="lg" /> : <h2>-</h2>}
|
||||||
|
color="info"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-clock" content={cilClock} />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.up_to_date')}
|
||||||
|
header={<h2>{data.latestSoftwareRate}</h2>}
|
||||||
|
color={getLatestColor(data.latestSoftwareRate)}
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
{getLatestIcon(data.latestSoftwareRate)}
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={t('common.devices')}
|
||||||
|
header={<h2>{data.numberOfDevices}</h2>}
|
||||||
|
color="primary"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} name="cil-router" />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CWidgetIcon
|
||||||
|
text={
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('firmware.average_age')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('firmware.age_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
header={<h2>{data.averageFirmwareAge}</h2>}
|
||||||
|
color="dark"
|
||||||
|
iconPadding={false}
|
||||||
|
>
|
||||||
|
<CIcon width={36} content={cilBirthdayCake} />
|
||||||
|
</CWidgetIcon>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.firmware_installed')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.firmwareDistribution.datasets}
|
||||||
|
labels={data.firmwareDistribution.labels}
|
||||||
|
options={{
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">
|
||||||
|
<div>
|
||||||
|
<div className="float-left">{t('common.devices_using_latest')}</div>
|
||||||
|
<div className="float-left ml-2">
|
||||||
|
<CPopover content={t('firmware.latest_explanation')}>
|
||||||
|
<CIcon content={cilInfo} />
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartBar
|
||||||
|
datasets={data.latest.datasets}
|
||||||
|
labels={data.latest.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">Unknown Firmware</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartHorizontalBar
|
||||||
|
datasets={data.unknownFirmwares.datasets}
|
||||||
|
labels={data.unknownFirmwares.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => value.split(' ')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.device_status')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.status.datasets}
|
||||||
|
labels={data.status.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) => `${ds.datasets[0].data[item.index]}%`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('firmware.device_types')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartPie
|
||||||
|
datasets={data.deviceType.datasets}
|
||||||
|
labels={data.deviceType.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (item, ds) => ds.labels[item[0].index],
|
||||||
|
label: (item, ds) =>
|
||||||
|
`${ds.datasets[0].data[item.index]} ${t('common.devices')}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">OUIs</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CChartHorizontalBar
|
||||||
|
datasets={data.ouis.datasets}
|
||||||
|
labels={data.ouis.labels}
|
||||||
|
options={{
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
position: 'right',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
beginAtZero: true,
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
callback: (value) => value.split(' ')[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
<CRow>
|
||||||
|
<CCol>
|
||||||
|
<CCard>
|
||||||
|
<CCardHeader className="dark-header">{t('common.endpoints')}</CCardHeader>
|
||||||
|
<CCardBody>
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
items={data.endpoints ?? []}
|
||||||
|
fields={columns}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
/>
|
||||||
|
</CCardBody>
|
||||||
|
</CCard>
|
||||||
|
</CCol>
|
||||||
|
<CCol />
|
||||||
|
<CCol />
|
||||||
|
</CRow>
|
||||||
|
</div>
|
||||||
|
{loading ? (
|
||||||
|
<div className={styles.centerContainer}>
|
||||||
|
<CSpinner className={styles.spinner} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FirmwareDashboard.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(FirmwareDashboard);
|
||||||
10
src/components/FirmwareDashboard/Dashboard/index.module.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.centerContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FirmwareDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
|
import { useAuth, COLOR_LIST } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
|
|
||||||
const FirmwareDashboard = () => {
|
const FirmwareDashboard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
36
src/components/FirmwareHistoryModal/Modal.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CDataTable } from '@coreui/react';
|
||||||
|
import { prettyDate } from 'utils/helper';
|
||||||
|
|
||||||
|
const FirmwareHistoryModal = ({ t, loading, data }) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'date', label: '#', _style: { width: '20%' } },
|
||||||
|
{ key: 'fromRelease', label: t('firmware.from_release'), sorter: false },
|
||||||
|
{ key: 'toRelease', label: t('firmware.to_release'), sorter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow table-sm"
|
||||||
|
fields={columns}
|
||||||
|
items={data}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
sorter
|
||||||
|
sorterValue={{ column: 'radio', asc: true }}
|
||||||
|
scopedSlots={{
|
||||||
|
date: (item) => <td>{prettyDate(item.upgraded)}</td>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FirmwareHistoryModal.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(FirmwareHistoryModal);
|
||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
CModalFooter,
|
CModalFooter,
|
||||||
CModalTitle,
|
CModalTitle,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import { FirmwareHistoryTable, useAuth } from 'ucentral-libs';
|
import { useAuth } from 'ucentral-libs';
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
|
const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -51,7 +52,7 @@ const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
|
|||||||
</CModalTitle>
|
</CModalTitle>
|
||||||
</CModalHeader>
|
</CModalHeader>
|
||||||
<CModalBody>
|
<CModalBody>
|
||||||
<FirmwareHistoryTable t={t} loading={loading} data={data} />
|
<Modal t={t} loading={loading} data={data} />
|
||||||
</CModalBody>
|
</CModalBody>
|
||||||
<CModalFooter>
|
<CModalFooter>
|
||||||
<CButton color="secondary" onClick={toggle}>
|
<CButton color="secondary" onClick={toggle}>
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ DeviceStatisticsChart.propTypes = {
|
|||||||
chart: PropTypes.instanceOf(Object).isRequired,
|
chart: PropTypes.instanceOf(Object).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeviceStatisticsChart;
|
export default React.memo(DeviceStatisticsChart);
|
||||||
|
|||||||
@@ -1,37 +1,104 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CSpinner } from '@coreui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as createUuid } from 'uuid';
|
import { v4 as createUuid } from 'uuid';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import { useAuth, useDevice } from 'ucentral-libs';
|
import { useAuth, useDevice } from 'ucentral-libs';
|
||||||
import { unixToTime, capitalizeFirstLetter } from 'utils/helper';
|
import {
|
||||||
import eventBus from 'utils/eventBus';
|
capitalizeFirstLetter,
|
||||||
|
datesSameDay,
|
||||||
|
dateToUnix,
|
||||||
|
prettyDate,
|
||||||
|
unixToTime,
|
||||||
|
} from 'utils/helper';
|
||||||
import DeviceStatisticsChart from './DeviceStatisticsChart';
|
import DeviceStatisticsChart from './DeviceStatisticsChart';
|
||||||
|
|
||||||
const StatisticsChartList = () => {
|
const StatisticsChartList = ({ setOptions, section, setStart, setEnd, time }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const { currentToken, endpoints } = useAuth();
|
const { currentToken, endpoints } = useAuth();
|
||||||
const { deviceSerialNumber } = useDevice();
|
const { deviceSerialNumber } = useDevice();
|
||||||
const [statOptions, setStatOptions] = useState({
|
const [statOptions, setStatOptions] = useState({
|
||||||
interfaceList: [],
|
interfaceList: [],
|
||||||
|
memory: [],
|
||||||
settings: {},
|
settings: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const transformIntoDataset = (data) => {
|
const transformIntoDataset = (data) => {
|
||||||
const sortedData = data.sort((a, b) => {
|
let sortedData = data.sort((a, b) => {
|
||||||
if (a.recorded > b.recorded) return 1;
|
if (a.recorded > b.recorded) return 1;
|
||||||
if (b.recorded > a.recorded) return -1;
|
if (b.recorded > a.recorded) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dataLength = sortedData.length;
|
||||||
|
if (dataLength > 1000 && dataLength < 3000) {
|
||||||
|
sortedData = sortedData.filter((dat, index) => index % 4 === 0);
|
||||||
|
} else if (dataLength >= 3000 && dataLength < 5000) {
|
||||||
|
sortedData = sortedData.filter((dat, index) => index % 8 === 0);
|
||||||
|
} else if (dataLength >= 5000 && dataLength < 7000) {
|
||||||
|
sortedData = sortedData.filter((dat, index) => index % 12 === 0);
|
||||||
|
} else if (dataLength > 7000) {
|
||||||
|
sortedData = sortedData.filter((dat, index) => index % 20 === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Looping through data to build our memory graph data
|
||||||
|
const memoryUsed = [
|
||||||
|
{
|
||||||
|
titleName: t('statistics.memory'),
|
||||||
|
name: 'Used',
|
||||||
|
backgroundColor: 'rgb(228,102,81,0.9)',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleName: t('statistics.memory'),
|
||||||
|
name: 'Buffered',
|
||||||
|
backgroundColor: 'rgb(228,102,81,0.9)',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleName: t('statistics.memory'),
|
||||||
|
name: 'Cached',
|
||||||
|
backgroundColor: 'rgb(228,102,81,0.9)',
|
||||||
|
data: [],
|
||||||
|
fill: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const log of sortedData) {
|
||||||
|
memoryUsed[0].data.push(
|
||||||
|
Math.floor((log.data.unit.memory.total - log.data.unit.memory.free) / 1024 / 1024),
|
||||||
|
);
|
||||||
|
memoryUsed[1].data.push(Math.floor(log.data.unit.memory.buffered / 1024 / 1024));
|
||||||
|
memoryUsed[2].data.push(Math.floor(log.data.unit.memory.cached / 1024 / 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUsed = memoryUsed[0].data;
|
||||||
|
if (newUsed.length > 0) newUsed.shift();
|
||||||
|
memoryUsed[0].data = newUsed;
|
||||||
|
const newBuff = memoryUsed[1].data;
|
||||||
|
if (newBuff.length > 0) newBuff.shift();
|
||||||
|
memoryUsed[1].data = newBuff;
|
||||||
|
const newCached = memoryUsed[2].data;
|
||||||
|
if (newCached.length > 0) newCached.shift();
|
||||||
|
memoryUsed[2].data = newCached;
|
||||||
|
|
||||||
// This dictionary will have a key that is the interface name and a value of it's index in the final array
|
// This dictionary will have a key that is the interface name and a value of it's index in the final array
|
||||||
const interfaceTypes = {};
|
const interfaceTypes = {};
|
||||||
const interfaceList = [];
|
const interfaceList = [];
|
||||||
const categories = [];
|
const categories = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
const areSameDay = datesSameDay(
|
||||||
|
new Date(sortedData[0].recorded * 1000),
|
||||||
|
new Date(sortedData[sortedData.length - 1].recorded * 1000),
|
||||||
|
);
|
||||||
|
|
||||||
// Just building the array for all the interfaces
|
// Just building the array for all the interfaces
|
||||||
for (const log of sortedData) {
|
for (const log of sortedData) {
|
||||||
categories.push(unixToTime(log.recorded));
|
categories.push(areSameDay ? unixToTime(log.recorded) : prettyDate(log.recorded));
|
||||||
for (const logInterface of log.data.interfaces) {
|
for (const logInterface of log.data.interfaces) {
|
||||||
if (interfaceTypes[logInterface.name] === undefined) {
|
if (interfaceTypes[logInterface.name] === undefined) {
|
||||||
interfaceTypes[logInterface.name] = i;
|
interfaceTypes[logInterface.name] = i;
|
||||||
@@ -57,22 +124,68 @@ const StatisticsChartList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Looping through all the data
|
// Looping through all the data
|
||||||
|
let prevTx = 0;
|
||||||
|
let prevRx = 0;
|
||||||
for (const log of sortedData) {
|
for (const log of sortedData) {
|
||||||
// Looping through the interfaces of the log
|
// Looping through the interfaces of the log
|
||||||
|
const version = log.data.version ?? 0;
|
||||||
for (const inter of log.data.interfaces) {
|
for (const inter of log.data.interfaces) {
|
||||||
|
if (inter.ssids?.length > 0) {
|
||||||
|
let totalTx = 0;
|
||||||
|
let totalRx = 0;
|
||||||
|
for (const ssid of inter.ssids) {
|
||||||
|
if (ssid.associations) {
|
||||||
|
for (const assoc of ssid.associations) {
|
||||||
|
if (version === 0) {
|
||||||
|
if (assoc.deltas) {
|
||||||
|
totalTx += assoc.deltas?.tx_bytes ?? 0;
|
||||||
|
totalRx += assoc.deltas?.rx_bytes ?? 0;
|
||||||
|
} else {
|
||||||
|
totalTx += assoc.tx_bytes ?? 0;
|
||||||
|
totalRx += assoc.rx_bytes ?? 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
totalTx += assoc.tx_bytes ?? 0;
|
||||||
|
totalRx += assoc.rx_bytes ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (version > 0) {
|
||||||
|
const tx = Math.floor(totalTx / 1024);
|
||||||
|
const rx = Math.floor(totalRx / 1024);
|
||||||
|
interfaceList[interfaceTypes[inter.name]][0].data.push(Math.max(tx - prevTx, 0));
|
||||||
|
interfaceList[interfaceTypes[inter.name]][1].data.push(Math.max(rx - prevRx, 0));
|
||||||
|
prevTx = tx;
|
||||||
|
prevRx = rx;
|
||||||
|
} else {
|
||||||
|
interfaceList[interfaceTypes[inter.name]][0].data.push(Math.floor(totalTx / 1024));
|
||||||
|
interfaceList[interfaceTypes[inter.name]][1].data.push(Math.floor(totalRx / 1024));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
interfaceList[interfaceTypes[inter.name]][0].data.push(
|
interfaceList[interfaceTypes[inter.name]][0].data.push(
|
||||||
inter.counters?.tx_bytes ? Math.floor(inter.counters.tx_bytes / 1024) : 0,
|
inter.counters ? Math.floor(inter.counters.tx_bytes) : 0,
|
||||||
);
|
);
|
||||||
interfaceList[interfaceTypes[inter.name]][1].data.push(
|
interfaceList[interfaceTypes[inter.name]][1].data.push(
|
||||||
inter.counters?.rx_bytes ? Math.floor(inter.counters.rx_bytes / 1024) : 0,
|
inter.counters ? Math.floor(inter.counters.rx_bytes) : 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
for (let y = 0; y < interfaceList.length; y += 1) {
|
||||||
|
for (let z = 0; z < interfaceList[y].length; z += 1) {
|
||||||
|
const newArray = interfaceList[y][z].data;
|
||||||
|
if (newArray.length > 0) newArray.shift();
|
||||||
|
interfaceList[y][z].data = newArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCategories = categories;
|
||||||
|
if (newCategories.length > 0) newCategories.shift();
|
||||||
|
const interfaceOptions = {
|
||||||
chart: {
|
chart: {
|
||||||
id: 'chart',
|
id: 'chart',
|
||||||
group: 'txrx',
|
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
curve: 'smooth',
|
curve: 'smooth',
|
||||||
@@ -84,8 +197,8 @@ const StatisticsChartList = () => {
|
|||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
categories,
|
categories: newCategories,
|
||||||
tickAmount: 20,
|
tickAmount: areSameDay ? 15 : 10,
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
labels: {
|
labels: {
|
||||||
@@ -105,61 +218,76 @@ const StatisticsChartList = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const memoryOptions = {
|
||||||
|
chart: {
|
||||||
|
id: 'chart',
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
tickAmount: areSameDay ? 15 : 10,
|
||||||
|
title: {
|
||||||
|
text: 'Time',
|
||||||
|
style: {
|
||||||
|
fontSize: '15px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
categories,
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
tickAmount: 5,
|
||||||
|
title: {
|
||||||
|
text: t('statistics.data_mb'),
|
||||||
|
style: {
|
||||||
|
fontSize: '15px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: 'top',
|
||||||
|
horizontalAlign: 'right',
|
||||||
|
float: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const newOptions = {
|
const newOptions = {
|
||||||
interfaceList,
|
interfaceList,
|
||||||
settings: options,
|
memory: [memoryUsed],
|
||||||
|
interfaceOptions,
|
||||||
|
memoryOptions,
|
||||||
|
start: new Date(sortedData[0].recorded * 1000).toISOString(),
|
||||||
|
end: new Date(sortedData[sortedData.length - 1].recorded * 1000).toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (statOptions !== newOptions) {
|
if (statOptions !== newOptions) {
|
||||||
setStatOptions(newOptions);
|
const sectionOptions = newOptions.interfaceList.map((opt) => ({
|
||||||
|
value: opt[0].titleName,
|
||||||
|
label: opt[0].titleName,
|
||||||
|
}));
|
||||||
|
setOptions([...sectionOptions, { value: 'memory', label: t('statistics.memory') }]);
|
||||||
|
setStatOptions({ ...newOptions });
|
||||||
|
if (sortedData.length > 0) {
|
||||||
|
setStart(new Date(sortedData[0].recorded * 1000));
|
||||||
|
setEnd(new Date(sortedData[sortedData.length - 1].recorded * 1000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatistics = () => {
|
const getInterface = useCallback(() => {
|
||||||
|
if (statOptions.interfaceList.length === 0) return <p>N/A</p>;
|
||||||
|
|
||||||
|
const interfaceToShow = statOptions.interfaceList.find(
|
||||||
|
(inter) => inter[0].titleName === section,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (interfaceToShow) {
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
data: interfaceToShow,
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
params: {
|
|
||||||
serialNumber: '24f5a207a130',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(
|
|
||||||
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics?newest=true&limit=50`,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
transformIntoDataset(response.data.data);
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (deviceSerialNumber) {
|
|
||||||
getStatistics();
|
|
||||||
}
|
|
||||||
}, [deviceSerialNumber]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
eventBus.on('refreshInterfaceStatistics', () => getStatistics());
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
eventBus.remove('refreshInterfaceStatistics');
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{statOptions.interfaceList.map((data) => {
|
|
||||||
const options = {
|
|
||||||
data,
|
|
||||||
options: {
|
options: {
|
||||||
...statOptions.settings,
|
...statOptions.interfaceOptions,
|
||||||
title: {
|
title: {
|
||||||
text: capitalizeFirstLetter(data[0].titleName),
|
text: capitalizeFirstLetter(interfaceToShow[0].titleName),
|
||||||
align: 'left',
|
align: 'left',
|
||||||
style: {
|
style: {
|
||||||
fontSize: '25px',
|
fontSize: '25px',
|
||||||
@@ -172,9 +300,92 @@ const StatisticsChartList = () => {
|
|||||||
<DeviceStatisticsChart chart={options} />
|
<DeviceStatisticsChart chart={options} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return <p>N/A</p>;
|
||||||
|
}, [statOptions, section]);
|
||||||
|
|
||||||
|
const getStatistics = () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let extraParams = '';
|
||||||
|
if (time.start !== null && time.end !== null) {
|
||||||
|
const utcStart = new Date(time.start).toISOString();
|
||||||
|
const utcEnd = new Date(time.end).toISOString();
|
||||||
|
options.params.startDate = dateToUnix(utcStart);
|
||||||
|
options.params.endDate = dateToUnix(utcEnd);
|
||||||
|
options.params.limit = 10000;
|
||||||
|
} else {
|
||||||
|
extraParams = '?newest=true&limit=50';
|
||||||
|
}
|
||||||
|
|
||||||
|
axiosInstance
|
||||||
|
.get(
|
||||||
|
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics${extraParams}`,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
transformIntoDataset(response.data.data);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (deviceSerialNumber) {
|
||||||
|
getStatistics();
|
||||||
|
}
|
||||||
|
}, [deviceSerialNumber, time.refreshId]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center">
|
||||||
|
<CSpinner size="xl" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{section !== 'memory' && !loading && getInterface()}
|
||||||
|
{section === 'memory' &&
|
||||||
|
!loading &&
|
||||||
|
statOptions.memory.map((data) => {
|
||||||
|
const options = {
|
||||||
|
data,
|
||||||
|
options: {
|
||||||
|
...statOptions.memoryOptions,
|
||||||
|
title: {
|
||||||
|
text: capitalizeFirstLetter(data[0].titleName),
|
||||||
|
align: 'left',
|
||||||
|
style: {
|
||||||
|
fontSize: '25px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div key={createUuid()}>
|
||||||
|
<DeviceStatisticsChart chart={options} section={section} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StatisticsChartList.propTypes = {
|
||||||
|
setOptions: PropTypes.func.isRequired,
|
||||||
|
section: PropTypes.string.isRequired,
|
||||||
|
time: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
setStart: PropTypes.func.isRequired,
|
||||||
|
setEnd: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default React.memo(StatisticsChartList);
|
export default React.memo(StatisticsChartList);
|
||||||
|
|||||||
@@ -1,30 +1,66 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CCard, CCardHeader, CCardBody, CPopover, CButton } from '@coreui/react';
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
import {
|
||||||
|
CCard,
|
||||||
|
CCardHeader,
|
||||||
|
CCardBody,
|
||||||
|
CPopover,
|
||||||
|
CButton,
|
||||||
|
CSelect,
|
||||||
|
CFormText,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import DatePicker from 'react-widgets/DatePicker';
|
||||||
import { cilSync } from '@coreui/icons';
|
import { cilSync } from '@coreui/icons';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import eventBus from 'utils/eventBus';
|
|
||||||
import LifetimeStatsmodal from 'components/LifetimeStatsModal';
|
|
||||||
import StatisticsChartList from './StatisticsChartList';
|
import StatisticsChartList from './StatisticsChartList';
|
||||||
import LatestStatisticsmodal from './LatestStatisticsModal';
|
import LatestStatisticsmodal from './LatestStatisticsModal';
|
||||||
|
|
||||||
const DeviceStatisticsCard = () => {
|
const DeviceStatisticsCard = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showLatestModal, setShowLatestModal] = useState(false);
|
const [showLatestModal, setShowLatestModal] = useState(false);
|
||||||
const [showLifetimeModal, setShowLifetimeModal] = useState(false);
|
const [options, setOptions] = useState([]);
|
||||||
|
const [section, setSection] = useState('');
|
||||||
|
const [start, setStart] = useState(null);
|
||||||
|
const [startError, setStartError] = useState(false);
|
||||||
|
const [end, setEnd] = useState(null);
|
||||||
|
const [endError, setEndError] = useState(false);
|
||||||
|
const [time, setTime] = useState({ refreshId: '0', start: null, end: null });
|
||||||
|
|
||||||
const toggleLatestModal = () => {
|
const toggleLatestModal = () => {
|
||||||
setShowLatestModal(!showLatestModal);
|
setShowLatestModal(!showLatestModal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleLifetimeModal = () => {
|
const modifyStart = (value) => {
|
||||||
setShowLifetimeModal(!showLifetimeModal);
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setStartError(false);
|
||||||
|
setStart(value);
|
||||||
|
} catch (e) {
|
||||||
|
setStart('');
|
||||||
|
setStartError(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifyEnd = (value) => {
|
||||||
|
try {
|
||||||
|
new Date(value).toISOString();
|
||||||
|
setEndError(false);
|
||||||
|
setEnd(value);
|
||||||
|
} catch (e) {
|
||||||
|
setEnd('');
|
||||||
|
setEndError(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
eventBus.dispatch('refreshInterfaceStatistics', { message: 'Refresh interface statistics' });
|
setTime({ refreshId: createUuid(), start, end });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (section === '' && options.length > 0) setSection(options[0].value);
|
||||||
|
}, [options]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CCard className="m-0">
|
<CCard className="m-0">
|
||||||
@@ -32,15 +68,46 @@ const DeviceStatisticsCard = () => {
|
|||||||
<div className="d-flex flex-row-reverse align-items-center">
|
<div className="d-flex flex-row-reverse align-items-center">
|
||||||
<div className="pl-2">
|
<div className="pl-2">
|
||||||
<CPopover content={t('common.refresh')}>
|
<CPopover content={t('common.refresh')}>
|
||||||
<CButton size="sm" color="info" onClick={refresh}>
|
<CButton size="sm" color="info" onClick={refresh} disabled={startError || endError}>
|
||||||
<CIcon content={cilSync} />
|
<CIcon content={cilSync} />
|
||||||
</CButton>
|
</CButton>
|
||||||
</CPopover>
|
</CPopover>
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-2">
|
<div className="pl-2">
|
||||||
<CButton size="sm" color="info" onClick={toggleLifetimeModal}>
|
<DatePicker
|
||||||
Lifetime Statistics
|
includeTime
|
||||||
</CButton>
|
onChange={(date) => modifyEnd(date)}
|
||||||
|
value={end ? new Date(end) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!endError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
To:
|
||||||
|
<div className="pl-2">
|
||||||
|
<DatePicker
|
||||||
|
includeTime
|
||||||
|
onChange={(date) => modifyStart(date)}
|
||||||
|
value={start ? new Date(start) : undefined}
|
||||||
|
/>
|
||||||
|
<CFormText color="danger" hidden={!startError}>
|
||||||
|
{t('common.invalid_date_explanation')}
|
||||||
|
</CFormText>
|
||||||
|
</div>
|
||||||
|
From:
|
||||||
|
<div className="px-2">
|
||||||
|
<CSelect
|
||||||
|
custom
|
||||||
|
value={section}
|
||||||
|
disabled={options.length === 0}
|
||||||
|
onChange={(e) => setSection(e.target.value)}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<option value={opt.value} key={createUuid()}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</CSelect>
|
||||||
</div>
|
</div>
|
||||||
<div className="pl-2">
|
<div className="pl-2">
|
||||||
<CButton size="sm" color="info" onClick={toggleLatestModal}>
|
<CButton size="sm" color="info" onClick={toggleLatestModal}>
|
||||||
@@ -50,11 +117,16 @@ const DeviceStatisticsCard = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CCardHeader>
|
</CCardHeader>
|
||||||
<CCardBody className="p-1">
|
<CCardBody className="p-1">
|
||||||
<StatisticsChartList />
|
<StatisticsChartList
|
||||||
|
setOptions={setOptions}
|
||||||
|
section={section}
|
||||||
|
time={time}
|
||||||
|
setStart={setStart}
|
||||||
|
setEnd={setEnd}
|
||||||
|
/>
|
||||||
</CCardBody>
|
</CCardBody>
|
||||||
</CCard>
|
</CCard>
|
||||||
<LatestStatisticsmodal show={showLatestModal} toggle={toggleLatestModal} />
|
<LatestStatisticsmodal show={showLatestModal} toggle={toggleLatestModal} />
|
||||||
<LifetimeStatsmodal show={showLifetimeModal} toggle={toggleLifetimeModal} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { LifetimeStatsModal as Modal, useAuth, useDevice } from 'ucentral-libs';
|
|
||||||
|
|
||||||
const LifetimeStatsModal = ({ show, toggle }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { currentToken, endpoints } = useAuth();
|
|
||||||
const { deviceSerialNumber } = useDevice();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [data, setData] = useState({});
|
|
||||||
|
|
||||||
const getData = () => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
Authorization: `Bearer ${currentToken}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
axiosInstance
|
|
||||||
.get(
|
|
||||||
`${endpoints.owgw}/api/v1/device/${deviceSerialNumber}/statistics?lifetime=true`,
|
|
||||||
options,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
setData(response.data);
|
|
||||||
})
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (show) getData();
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
return <Modal t={t} loading={loading} show={show} toggle={toggle} data={data} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
LifetimeStatsModal.propTypes = {
|
|
||||||
show: PropTypes.bool.isRequired,
|
|
||||||
toggle: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LifetimeStatsModal;
|
|
||||||
55
src/components/NetworkDiagram/Graph.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactFlow, { removeElements, MiniMap, Controls, Background } from 'react-flow-renderer';
|
||||||
|
|
||||||
|
const NetworkDiagram = ({ show, elements, setElements }) => {
|
||||||
|
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||||
|
const onElementsRemove = (elementsToRemove) => {
|
||||||
|
setElements((els) => removeElements(elementsToRemove, els));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLoad = (instance) => {
|
||||||
|
setReactFlowInstance(instance);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show && reactFlowInstance !== null && elements.length > 0) {
|
||||||
|
setTimeout(() => reactFlowInstance.fitView(), 100);
|
||||||
|
}
|
||||||
|
}, [show, reactFlowInstance, elements]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ height: '80vh', width: '100%' }}>
|
||||||
|
<ReactFlow
|
||||||
|
elements={elements}
|
||||||
|
onElementsRemove={onElementsRemove}
|
||||||
|
onLoad={onLoad}
|
||||||
|
snapToGrid
|
||||||
|
snapGrid={[20, 20]}
|
||||||
|
>
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(n) => {
|
||||||
|
if (n.style?.background) return n.style.background;
|
||||||
|
|
||||||
|
return '#fff';
|
||||||
|
}}
|
||||||
|
nodeBorderRadius={5}
|
||||||
|
/>
|
||||||
|
<Controls />
|
||||||
|
<Background color="#aaa" gap={20} />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkDiagram.propTypes = {
|
||||||
|
show: PropTypes.bool,
|
||||||
|
elements: PropTypes.instanceOf(Array).isRequired,
|
||||||
|
setElements: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkDiagram.defaultProps = {
|
||||||
|
show: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(NetworkDiagram);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CRow, CCol } from '@coreui/react';
|
import { CRow, CCol } from '@coreui/react';
|
||||||
import { NetworkDiagram as Graph } from 'ucentral-libs';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import createLayoutedElements from './dagreAdapter';
|
import createLayoutedElements from './dagreAdapter';
|
||||||
|
import Graph from './Graph';
|
||||||
|
|
||||||
const associationStyle = {
|
const associationStyle = {
|
||||||
background: '#3399ff',
|
background: '#3399ff',
|
||||||
|
|||||||
42
src/components/WifiAnalysis/RadioAnalysis.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CDataTable } from '@coreui/react';
|
||||||
|
|
||||||
|
const RadioAnalysisTable = ({ data, loading }) => {
|
||||||
|
const columns = [
|
||||||
|
{ key: 'radio', label: '#', _style: { width: '5%' } },
|
||||||
|
{ key: 'channel', label: 'Ch', _style: { width: '5%' } },
|
||||||
|
{ key: 'channelWidth', label: 'C Width', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'noise', label: 'Noise', _style: { width: '4%' }, sorter: false },
|
||||||
|
{ key: 'txPower', label: 'Tx Power', _style: { width: '9%' }, sorter: false },
|
||||||
|
{ key: 'activeMs', label: 'Active MS', _style: { width: '23%' }, sorter: false },
|
||||||
|
{ key: 'busyMs', label: 'Busy MS', _style: { width: '23%' }, sorter: false },
|
||||||
|
{ key: 'receiveMs', label: 'Receive MS', _style: { width: '23%' }, sorter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const centerIfEmpty = (value) => (
|
||||||
|
<td className={!value || value === '' || value === '-' ? 'text-center' : ''}>{value}</td>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="table-sm"
|
||||||
|
fields={columns}
|
||||||
|
items={data}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
sorter
|
||||||
|
sorterValue={{ column: 'radio', asc: true }}
|
||||||
|
scopedSlots={{
|
||||||
|
noise: (item) => centerIfEmpty(item.noise),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RadioAnalysisTable.propTypes = {
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
export default RadioAnalysisTable;
|
||||||
146
src/components/WifiAnalysis/WifiAnalysis.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CDataTable,
|
||||||
|
CPopover,
|
||||||
|
CModal,
|
||||||
|
CModalHeader,
|
||||||
|
CModalTitle,
|
||||||
|
CModalBody,
|
||||||
|
} from '@coreui/react';
|
||||||
|
import CIcon from '@coreui/icons-react';
|
||||||
|
import { cilX } from '@coreui/icons';
|
||||||
|
import { v4 as createUuid } from 'uuid';
|
||||||
|
|
||||||
|
const WifiAnalysisTable = ({ t, data, loading }) => {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const [ips, setIps] = useState(null);
|
||||||
|
|
||||||
|
const toggle = (ssid, v4, v6) => {
|
||||||
|
if (v4 && v6) setIps({ ssid, v4, v6 });
|
||||||
|
setShow(!show);
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{ key: 'radio', label: '#', _style: { width: '1%' } },
|
||||||
|
{ key: 'station', label: 'Station ID', _style: { width: '1%' } },
|
||||||
|
{ key: 'vendor', label: t('wifi_analysis.vendor'), _style: { width: '15%' }, sorter: false },
|
||||||
|
{ key: 'mode', label: t('wifi_analysis.mode'), _style: { width: '1%' }, sorter: false },
|
||||||
|
{ key: 'ssid', label: 'SSID', _style: { width: '15%' } },
|
||||||
|
{ key: 'rssi', label: 'RSSI', _style: { width: '5%' }, sorter: false },
|
||||||
|
{ key: 'rxRate', label: 'Rx Rate', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'rxBytes', label: 'Rx', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'rxMcs', label: 'Rx MCS', _style: { width: '6%' }, sorter: false },
|
||||||
|
{ key: 'rxNss', label: 'Rx NSS', _style: { width: '6%' }, sorter: false },
|
||||||
|
{ key: 'txRate', label: 'Tx Rate', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'txBytes', label: 'Tx', _style: { width: '7%' }, sorter: false },
|
||||||
|
{ key: 'ips', label: 'IP', _style: { width: '1%' }, sorter: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const centerIfEmpty = (value) => (
|
||||||
|
<td
|
||||||
|
className={
|
||||||
|
!value || value === '' || value === '-'
|
||||||
|
? 'text-center align-middle'
|
||||||
|
: 'text-right align-middle'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayIp = (ssid, v4, v6) => {
|
||||||
|
const count = v4.length + v6.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td className="ignore-overflow text-center align-middle">
|
||||||
|
{count > 0 ? (
|
||||||
|
<CPopover content="View">
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggle(ssid, v4, v6)}
|
||||||
|
className="py-1"
|
||||||
|
>
|
||||||
|
{count}
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
) : (
|
||||||
|
count
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CDataTable
|
||||||
|
addTableClasses="ignore-overflow mb-5 table-sm"
|
||||||
|
fields={columns}
|
||||||
|
items={data}
|
||||||
|
hover
|
||||||
|
border
|
||||||
|
loading={loading}
|
||||||
|
sorter
|
||||||
|
sorterValue={{ column: 'radio', asc: true }}
|
||||||
|
scopedSlots={{
|
||||||
|
station: (item) => (
|
||||||
|
<td
|
||||||
|
className="text-center align-middle"
|
||||||
|
style={{ fontFamily: 'monospace', fontSize: '0.96rem' }}
|
||||||
|
>
|
||||||
|
{item.station}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
radio: (item) => <td className="text-center align-middle">{item.radio.radio}</td>,
|
||||||
|
ssid: (item) => <td className="align-middle">{item.ssid}</td>,
|
||||||
|
mode: (item) => <td className="align-middle">{item.mode}</td>,
|
||||||
|
vendor: (item) => <td className="align-middle">{item.vendor}</td>,
|
||||||
|
rxMcs: (item) => centerIfEmpty(item.rxMcs),
|
||||||
|
rxRate: (item) => centerIfEmpty(item.rxRate),
|
||||||
|
rxBytes: (item) => centerIfEmpty(item.rxBytes),
|
||||||
|
txRate: (item) => centerIfEmpty(item.txRate),
|
||||||
|
txBytes: (item) => centerIfEmpty(item.txBytes),
|
||||||
|
rxNss: (item) => centerIfEmpty(item.rxNss),
|
||||||
|
rssi: (item) => centerIfEmpty(item.rssi),
|
||||||
|
ips: (item) => displayIp(item.ssid, item.ipV4, item.ipV6),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CModal size="lg" show={show} onClose={toggle}>
|
||||||
|
<CModalHeader className="p-1">
|
||||||
|
<CModalTitle className="pl-1 pt-1">{ips?.ssid}</CModalTitle>
|
||||||
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.close')}>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
|
<CIcon content={cilX} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
</CModalHeader>
|
||||||
|
<CModalBody>
|
||||||
|
<div>
|
||||||
|
IpV4
|
||||||
|
<ul>
|
||||||
|
{ips?.v4?.map((ip) => (
|
||||||
|
<li key={createUuid()}>{ip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
IpV6
|
||||||
|
<ul>
|
||||||
|
{ips?.v6?.map((ip) => (
|
||||||
|
<li key={createUuid()}>{ip}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CModalBody>
|
||||||
|
</CModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WifiAnalysisTable.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
data: PropTypes.instanceOf(Object).isRequired,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
export default WifiAnalysisTable;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { WifiAnalysisTable, RadioAnalysisTable, useAuth } from 'ucentral-libs';
|
import { useAuth } from 'ucentral-libs';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import NetworkDiagram from 'components/NetworkDiagram';
|
import NetworkDiagram from 'components/NetworkDiagram';
|
||||||
import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper';
|
import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper';
|
||||||
@@ -19,7 +19,9 @@ import {
|
|||||||
CPopover,
|
CPopover,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilX } from '@coreui/icons';
|
import { cilSync, cilX } from '@coreui/icons';
|
||||||
|
import RadioAnalysisTable from './RadioAnalysis';
|
||||||
|
import WifiAnalysisTable from './WifiAnalysis';
|
||||||
|
|
||||||
const parseDbm = (value) => {
|
const parseDbm = (value) => {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
@@ -65,9 +67,33 @@ const WifiAnalysis = () => {
|
|||||||
return ips;
|
return ips;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseAssociationStats = (json) => {
|
const getVendors = async (bssids) => {
|
||||||
|
setLoading(true);
|
||||||
|
setRange(19);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
Authorization: `Bearer ${currentToken}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return axiosInstance
|
||||||
|
.get(`${endpoints.owgw}/api/v1/ouis?macList=${bssids.join(',')}`, options)
|
||||||
|
.then((response) => {
|
||||||
|
const newObj = bssids;
|
||||||
|
for (const tag of response.data.tagList) {
|
||||||
|
newObj[tag.tag] = tag.value === '' ? '-' : tag.value;
|
||||||
|
}
|
||||||
|
return newObj;
|
||||||
|
})
|
||||||
|
.catch(() => ({}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseAssociationStats = async (json) => {
|
||||||
const newParsedAssociationStats = [];
|
const newParsedAssociationStats = [];
|
||||||
const newParsedRadioStats = [];
|
const newParsedRadioStats = [];
|
||||||
|
const bssidObj = {};
|
||||||
|
|
||||||
for (const stat of json.data) {
|
for (const stat of json.data) {
|
||||||
const associations = [];
|
const associations = [];
|
||||||
@@ -94,7 +120,7 @@ const WifiAnalysis = () => {
|
|||||||
|
|
||||||
// Looping through the interfaces
|
// Looping through the interfaces
|
||||||
for (const deviceInterface of stat.data.interfaces) {
|
for (const deviceInterface of stat.data.interfaces) {
|
||||||
if ('counters' in deviceInterface && 'ssids' in deviceInterface) {
|
if ('ssids' in deviceInterface) {
|
||||||
for (const ssid of deviceInterface.ssids) {
|
for (const ssid of deviceInterface.ssids) {
|
||||||
// Information common between all associations
|
// Information common between all associations
|
||||||
const radioInfo = {
|
const radioInfo = {
|
||||||
@@ -121,10 +147,12 @@ const WifiAnalysis = () => {
|
|||||||
|
|
||||||
if ('associations' in ssid) {
|
if ('associations' in ssid) {
|
||||||
for (const association of ssid.associations) {
|
for (const association of ssid.associations) {
|
||||||
|
bssidObj[association.station] = 0;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
radio: radioInfo,
|
radio: radioInfo,
|
||||||
...extractIp(stat.data, association.bssid),
|
...extractIp(stat.data, association.bssid),
|
||||||
bssid: association.bssid,
|
station: association.station,
|
||||||
ssid: ssid.ssid,
|
ssid: ssid.ssid,
|
||||||
rssi: association.rssi ? parseDbm(association.rssi) : '-',
|
rssi: association.rssi ? parseDbm(association.rssi) : '-',
|
||||||
mode: ssid.mode,
|
mode: ssid.mode,
|
||||||
@@ -147,6 +175,16 @@ const WifiAnalysis = () => {
|
|||||||
newParsedAssociationStats.push(associations);
|
newParsedAssociationStats.push(associations);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding Vendor info to associations
|
||||||
|
const vendors = await getVendors(Object.keys(bssidObj));
|
||||||
|
|
||||||
|
for (let i = 0; i < newParsedAssociationStats.length; i += 1) {
|
||||||
|
for (let y = 0; y < newParsedAssociationStats[i].length; y += 1) {
|
||||||
|
newParsedAssociationStats[i][y].vendor =
|
||||||
|
vendors[newParsedAssociationStats[i][y].station] ?? '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Radio Stats
|
// Radio Stats
|
||||||
const ascOrderedRadioStats = newParsedRadioStats.reverse();
|
const ascOrderedRadioStats = newParsedRadioStats.reverse();
|
||||||
setParsedRadioStats(ascOrderedRadioStats);
|
setParsedRadioStats(ascOrderedRadioStats);
|
||||||
@@ -201,14 +239,19 @@ const WifiAnalysis = () => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CCard>
|
<CCard>
|
||||||
<CCardHeader className="dark-header">
|
<CCardHeader className="dark-header d-flex flex-row-reverse align-items-center">
|
||||||
<CRow>
|
<div className="pl-2">
|
||||||
<CCol className="text-right">
|
<CPopover content={t('common.refresh')}>
|
||||||
|
<CButton size="sm" color="info" onClick={getLatestAssociationStats}>
|
||||||
|
<CIcon content={cilSync} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
<CButton color="info" size="sm" onClick={toggleModal}>
|
<CButton color="info" size="sm" onClick={toggleModal}>
|
||||||
{t('wifi_analysis.network_diagram')}
|
{t('wifi_analysis.network_diagram')}
|
||||||
</CButton>
|
</CButton>
|
||||||
</CCol>
|
</div>
|
||||||
</CRow>
|
|
||||||
</CCardHeader>
|
</CCardHeader>
|
||||||
<CCardBody>
|
<CCardBody>
|
||||||
<CRow className="mb-4">
|
<CRow className="mb-4">
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
CAlert,
|
||||||
CButton,
|
CButton,
|
||||||
CModal,
|
CModal,
|
||||||
CModalHeader,
|
CModalHeader,
|
||||||
CModalTitle,
|
CModalTitle,
|
||||||
CModalBody,
|
CModalBody,
|
||||||
CModalFooter,
|
|
||||||
CRow,
|
CRow,
|
||||||
CForm,
|
CForm,
|
||||||
CSwitch,
|
CSwitch,
|
||||||
@@ -13,15 +13,17 @@ import {
|
|||||||
CPopover,
|
CPopover,
|
||||||
} from '@coreui/react';
|
} from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilX } from '@coreui/icons';
|
import { cilCloudDownload, cilGauge, cilX } from '@coreui/icons';
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import axiosInstance from 'utils/axiosInstance';
|
import axiosInstance from 'utils/axiosInstance';
|
||||||
import eventBus from 'utils/eventBus';
|
import eventBus from 'utils/eventBus';
|
||||||
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
|
import { prettyDateForFile } from 'utils/helper';
|
||||||
|
import { useAuth, useDevice } from 'ucentral-libs';
|
||||||
import WifiChannelTable from 'components/WifiScanResultModal/WifiChannelTable';
|
import WifiChannelTable from 'components/WifiScanResultModal/WifiChannelTable';
|
||||||
import 'react-widgets/styles.css';
|
import 'react-widgets/styles.css';
|
||||||
|
import { CSVLink } from 'react-csv';
|
||||||
|
|
||||||
const WifiScanModal = ({ show, toggleModal }) => {
|
const WifiScanModal = ({ show, toggleModal }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -29,14 +31,16 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
const { deviceSerialNumber } = useDevice();
|
const { deviceSerialNumber } = useDevice();
|
||||||
const [hadSuccess, setHadSuccess] = useState(false);
|
const [hadSuccess, setHadSuccess] = useState(false);
|
||||||
const [hadFailure, setHadFailure] = useState(false);
|
const [hadFailure, setHadFailure] = useState(false);
|
||||||
|
const [errorCode, setErrorCode] = useState(0);
|
||||||
const [waiting, setWaiting] = useState(false);
|
const [waiting, setWaiting] = useState(false);
|
||||||
const [choseVerbose, setVerbose] = useState(true);
|
const [dfs, setDfs] = useState(true);
|
||||||
const [activeScan, setActiveScan] = useState(false);
|
const [activeScan, setActiveScan] = useState(false);
|
||||||
const [hideOptions, setHideOptions] = useState(false);
|
const [hideOptions, setHideOptions] = useState(false);
|
||||||
const [channelList, setChannelList] = useState([]);
|
const [channelList, setChannelList] = useState([]);
|
||||||
|
const [csvData, setCsvData] = useState(null);
|
||||||
|
|
||||||
const toggleVerbose = () => {
|
const toggleDfs = () => {
|
||||||
setVerbose(!choseVerbose);
|
setDfs(!dfs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleActiveScan = () => {
|
const toggleActiveScan = () => {
|
||||||
@@ -48,9 +52,11 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
setHadFailure(false);
|
setHadFailure(false);
|
||||||
setWaiting(false);
|
setWaiting(false);
|
||||||
setChannelList([]);
|
setChannelList([]);
|
||||||
setVerbose(true);
|
setCsvData(null);
|
||||||
|
setDfs(true);
|
||||||
setActiveScan(false);
|
setActiveScan(false);
|
||||||
setHideOptions(false);
|
setHideOptions(false);
|
||||||
|
setErrorCode(0);
|
||||||
}, [show]);
|
}, [show]);
|
||||||
|
|
||||||
const parseThroughList = (scanList) => {
|
const parseThroughList = (scanList) => {
|
||||||
@@ -73,7 +79,10 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
scanList.forEach((device) => {
|
scanList.forEach((device) => {
|
||||||
if (device.channel === channelNumber) {
|
if (device.channel === channelNumber) {
|
||||||
const deviceToAdd = {};
|
const deviceToAdd = {};
|
||||||
deviceToAdd.SSID = device.ssid ?? 'N/A';
|
if (device.ssid && device.ssid.length > 0) deviceToAdd.SSID = device.ssid;
|
||||||
|
else {
|
||||||
|
deviceToAdd.SSID = device.meshid && device.meshid.length > 0 ? device.meshid : 'N/A';
|
||||||
|
}
|
||||||
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
|
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
|
||||||
channel.devices.push(deviceToAdd);
|
channel.devices.push(deviceToAdd);
|
||||||
}
|
}
|
||||||
@@ -84,6 +93,40 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
return finalList;
|
return finalList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getData = useCallback((scanResults) => {
|
||||||
|
if (scanResults === null || scanResults.length === 0) return [];
|
||||||
|
const dbmNumber = 4294967295;
|
||||||
|
const listOfChannels = [];
|
||||||
|
const listCsv = [];
|
||||||
|
|
||||||
|
scanResults.forEach((scan) => {
|
||||||
|
if (!listOfChannels.includes(scan.channel)) {
|
||||||
|
listOfChannels.push(scan.channel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listOfChannels.forEach((channelNumber) => {
|
||||||
|
const channel = {
|
||||||
|
channel: channelNumber,
|
||||||
|
devices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
scanResults.forEach((device) => {
|
||||||
|
if (device.channel === channelNumber) {
|
||||||
|
const deviceToAdd = {};
|
||||||
|
if (device.ssid && device.ssid.length > 0) deviceToAdd.SSID = device.ssid;
|
||||||
|
else {
|
||||||
|
deviceToAdd.SSID = device.meshid && device.meshid.length > 0 ? device.meshid : 'N/A';
|
||||||
|
}
|
||||||
|
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
|
||||||
|
channel.devices.push(deviceToAdd);
|
||||||
|
listCsv.push({ ...deviceToAdd, ...device });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return listCsv;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const doAction = () => {
|
const doAction = () => {
|
||||||
setHadFailure(false);
|
setHadFailure(false);
|
||||||
setHadSuccess(false);
|
setHadSuccess(false);
|
||||||
@@ -91,7 +134,7 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
|
|
||||||
const parameters = {
|
const parameters = {
|
||||||
serialNumber: deviceSerialNumber,
|
serialNumber: deviceSerialNumber,
|
||||||
verbose: choseVerbose,
|
override_dfs: dfs,
|
||||||
activeScan,
|
activeScan,
|
||||||
};
|
};
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -110,6 +153,8 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
|
|
||||||
if (scanList) {
|
if (scanList) {
|
||||||
setChannelList(parseThroughList(scanList));
|
setChannelList(parseThroughList(scanList));
|
||||||
|
setCsvData(getData(scanList));
|
||||||
|
setErrorCode(response.data.errorCode ?? 0);
|
||||||
setHideOptions(true);
|
setHideOptions(true);
|
||||||
setHadSuccess(true);
|
setHadSuccess(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -130,6 +175,35 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
<CModalHeader className="p-1">
|
<CModalHeader className="p-1">
|
||||||
<CModalTitle className="pl-1 pt-1">{t('actions.wifi_scan')}</CModalTitle>
|
<CModalTitle className="pl-1 pt-1">{t('actions.wifi_scan')}</CModalTitle>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
{csvData !== null ? (
|
||||||
|
<CPopover content={t('common.download')}>
|
||||||
|
<CSVLink
|
||||||
|
filename={`wifi_scan_${deviceSerialNumber}_${prettyDateForFile(
|
||||||
|
new Date().getTime() / 1000,
|
||||||
|
)}.csv`}
|
||||||
|
data={csvData}
|
||||||
|
>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2">
|
||||||
|
<CIcon content={cilCloudDownload} />
|
||||||
|
</CButton>
|
||||||
|
</CSVLink>
|
||||||
|
</CPopover>
|
||||||
|
) : (
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2" disabled>
|
||||||
|
<CIcon content={cilCloudDownload} />
|
||||||
|
</CButton>
|
||||||
|
)}
|
||||||
|
<CPopover content={t('scan.scan')}>
|
||||||
|
<CButton
|
||||||
|
color="primary"
|
||||||
|
variant="outline"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={doAction}
|
||||||
|
disabled={waiting}
|
||||||
|
>
|
||||||
|
<CIcon content={cilGauge} />
|
||||||
|
</CButton>
|
||||||
|
</CPopover>
|
||||||
<CPopover content={t('common.close')}>
|
<CPopover content={t('common.close')}>
|
||||||
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggleModal}>
|
||||||
<CIcon content={cilX} />
|
<CIcon content={cilX} />
|
||||||
@@ -142,14 +216,14 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
<h6>{t('scan.directions')}</h6>
|
<h6>{t('scan.directions')}</h6>
|
||||||
<CRow className="mt-3">
|
<CRow className="mt-3">
|
||||||
<CCol md="3">
|
<CCol md="3">
|
||||||
<p className="pl-2">Verbose:</p>
|
<p className="pl-2">{t('wifi_analysis.override_dfs')}:</p>
|
||||||
</CCol>
|
</CCol>
|
||||||
<CCol>
|
<CCol>
|
||||||
<CForm className="pl-4">
|
<CForm className="pl-4">
|
||||||
<CSwitch
|
<CSwitch
|
||||||
color="primary"
|
color="primary"
|
||||||
defaultChecked={choseVerbose}
|
defaultChecked={dfs}
|
||||||
onClick={toggleVerbose}
|
onClick={toggleDfs}
|
||||||
labelOn={t('common.on')}
|
labelOn={t('common.on')}
|
||||||
labelOff={t('common.off')}
|
labelOff={t('common.off')}
|
||||||
/>
|
/>
|
||||||
@@ -191,23 +265,16 @@ const WifiScanModal = ({ show, toggleModal }) => {
|
|||||||
<h6>{t('scan.result_directions')}</h6>
|
<h6>{t('scan.result_directions')}</h6>
|
||||||
</CCol>
|
</CCol>
|
||||||
</CRow>
|
</CRow>
|
||||||
|
{errorCode === 1 && (
|
||||||
|
<CRow className="mb-2">
|
||||||
|
<CCol>
|
||||||
|
<CAlert color="warning">{t('wifi_analysis.scan_warning')}</CAlert>
|
||||||
|
</CCol>
|
||||||
|
</CRow>
|
||||||
|
)}
|
||||||
<WifiChannelTable channels={channelList} />
|
<WifiChannelTable channels={channelList} />
|
||||||
</div>
|
</div>
|
||||||
</CModalBody>
|
</CModalBody>
|
||||||
<CModalFooter>
|
|
||||||
<LoadingButton
|
|
||||||
label={!hadSuccess && !hadFailure ? t('scan.scan') : t('scan.re_scan')}
|
|
||||||
isLoadingLabel={t('scan.scanning')}
|
|
||||||
isLoading={waiting}
|
|
||||||
action={doAction}
|
|
||||||
variant="outline"
|
|
||||||
block={false}
|
|
||||||
disabled={waiting}
|
|
||||||
/>
|
|
||||||
<CButton color="secondary" onClick={toggleModal}>
|
|
||||||
{!hadSuccess && !hadFailure ? t('common.cancel') : t('common.exit')}
|
|
||||||
</CButton>
|
|
||||||
</CModalFooter>
|
|
||||||
</CModal>
|
</CModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,58 @@
|
|||||||
/* eslint-disable-rule prefer-destructuring */
|
/* eslint-disable-rule prefer-destructuring */
|
||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
|
import { CButton, CModal, CModalHeader, CModalBody, CModalTitle, CPopover } from '@coreui/react';
|
||||||
import CIcon from '@coreui/icons-react';
|
import CIcon from '@coreui/icons-react';
|
||||||
import { cilX } from '@coreui/icons';
|
import { cilCloudDownload, cilX } from '@coreui/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { prettyDate } from 'utils/helper';
|
import { prettyDate, prettyDateForFile } from 'utils/helper';
|
||||||
|
import { useDevice } from 'ucentral-libs';
|
||||||
|
import { CSVLink } from 'react-csv';
|
||||||
import WifiChannelTable from './WifiChannelTable';
|
import WifiChannelTable from './WifiChannelTable';
|
||||||
|
|
||||||
const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
|
const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { deviceSerialNumber } = useDevice();
|
||||||
|
|
||||||
const parseThroughList = (scanList) => {
|
const getData = useCallback(() => {
|
||||||
|
if (scanResults === null || scanResults.length === 0) return [];
|
||||||
|
const dbmNumber = 4294967295;
|
||||||
|
const listOfChannels = [];
|
||||||
|
const listCsv = [];
|
||||||
|
|
||||||
|
scanResults.forEach((scan) => {
|
||||||
|
if (!listOfChannels.includes(scan.channel)) {
|
||||||
|
listOfChannels.push(scan.channel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listOfChannels.forEach((channelNumber) => {
|
||||||
|
const channel = {
|
||||||
|
channel: channelNumber,
|
||||||
|
devices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
scanResults.forEach((device) => {
|
||||||
|
if (device.channel === channelNumber) {
|
||||||
|
const deviceToAdd = {};
|
||||||
|
if (device.ssid && device.ssid.length > 0) deviceToAdd.SSID = device.ssid;
|
||||||
|
else {
|
||||||
|
deviceToAdd.SSID = device.meshid && device.meshid.length > 0 ? device.meshid : 'N/A';
|
||||||
|
}
|
||||||
|
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
|
||||||
|
channel.devices.push(deviceToAdd);
|
||||||
|
listCsv.push({ ...deviceToAdd, ...device });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return listCsv;
|
||||||
|
}, [scanResults]);
|
||||||
|
|
||||||
|
const parseThroughList = useCallback(() => {
|
||||||
const dbmNumber = 4294967295;
|
const dbmNumber = 4294967295;
|
||||||
const listOfChannels = [];
|
const listOfChannels = [];
|
||||||
|
|
||||||
scanList.forEach((scan) => {
|
scanResults.forEach((scan) => {
|
||||||
if (!listOfChannels.includes(scan.channel)) {
|
if (!listOfChannels.includes(scan.channel)) {
|
||||||
listOfChannels.push(scan.channel);
|
listOfChannels.push(scan.channel);
|
||||||
}
|
}
|
||||||
@@ -28,10 +65,13 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
|
|||||||
devices: [],
|
devices: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
scanList.forEach((device) => {
|
scanResults.forEach((device) => {
|
||||||
if (device.channel === channelNumber) {
|
if (device.channel === channelNumber) {
|
||||||
const deviceToAdd = {};
|
const deviceToAdd = {};
|
||||||
deviceToAdd.SSID = device.ssid ?? 'N/A';
|
if (device.ssid && device.ssid.length > 0) deviceToAdd.SSID = device.ssid;
|
||||||
|
else {
|
||||||
|
deviceToAdd.SSID = device.meshid && device.meshid.length > 0 ? device.meshid : 'N/A';
|
||||||
|
}
|
||||||
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
|
deviceToAdd.Signal = (dbmNumber - device.signal) * -1;
|
||||||
channel.devices.push(deviceToAdd);
|
channel.devices.push(deviceToAdd);
|
||||||
}
|
}
|
||||||
@@ -40,7 +80,8 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
|
|||||||
finalList.push(channel);
|
finalList.push(channel);
|
||||||
});
|
});
|
||||||
return finalList;
|
return finalList;
|
||||||
};
|
}, [scanResults]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CModal size="lg" show={show} onClose={toggle}>
|
<CModal size="lg" show={show} onClose={toggle}>
|
||||||
<CModalHeader>
|
<CModalHeader>
|
||||||
@@ -48,6 +89,18 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
|
|||||||
{date !== '' ? prettyDate(date) : ''} {t('scan.results')}
|
{date !== '' ? prettyDate(date) : ''} {t('scan.results')}
|
||||||
</CModalTitle>
|
</CModalTitle>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
<CPopover content={t('common.download')}>
|
||||||
|
<CSVLink
|
||||||
|
filename={`wifi_scan_${deviceSerialNumber}_${
|
||||||
|
date !== '' ? prettyDateForFile(date) : ''
|
||||||
|
}.csv`}
|
||||||
|
data={getData()}
|
||||||
|
>
|
||||||
|
<CButton color="primary" variant="outline" className="ml-2">
|
||||||
|
<CIcon content={cilCloudDownload} />
|
||||||
|
</CButton>
|
||||||
|
</CSVLink>
|
||||||
|
</CPopover>
|
||||||
<CPopover content={t('common.close')}>
|
<CPopover content={t('common.close')}>
|
||||||
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
<CButton color="primary" variant="outline" className="ml-2" onClick={toggle}>
|
||||||
<CIcon content={cilX} />
|
<CIcon content={cilX} />
|
||||||
@@ -56,9 +109,7 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
|
|||||||
</div>
|
</div>
|
||||||
</CModalHeader>
|
</CModalHeader>
|
||||||
<CModalBody>
|
<CModalBody>
|
||||||
{scanResults === null ? null : (
|
{scanResults === null ? null : <WifiChannelTable channels={parseThroughList()} />}
|
||||||
<WifiChannelTable channels={parseThroughList(scanResults)} />
|
|
||||||
)}
|
|
||||||
</CModalBody>
|
</CModalBody>
|
||||||
</CModal>
|
</CModal>
|
||||||
);
|
);
|
||||||
|
|||||||