mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
synced 2025-10-30 18:27:53 +00:00
Compare commits
38 Commits
release/v2
...
release/v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
177d24e508 | ||
|
|
69b0d1ee9d | ||
|
|
ef52497b04 | ||
|
|
039e641046 | ||
|
|
f1f62efe6f | ||
|
|
b3053f32b2 | ||
|
|
09184b0402 | ||
|
|
98562fd967 | ||
|
|
65e9e64cb4 | ||
|
|
573ecbd58d | ||
|
|
a801fcca49 | ||
|
|
e9d16ee172 | ||
|
|
db4dfc93e8 | ||
|
|
975b715a7c | ||
|
|
cf17f03ae0 | ||
|
|
64f3ee797e | ||
|
|
e287705e88 | ||
|
|
9583b2bae0 | ||
|
|
2698993a6d | ||
|
|
a14b595e8c | ||
|
|
d7957b85ae | ||
|
|
227a51423d | ||
|
|
ea0e7340cc | ||
|
|
999680e94b | ||
|
|
566dbbb157 | ||
|
|
75d995d54e | ||
|
|
908faa491b | ||
|
|
7a254e343e | ||
|
|
016ac336b9 | ||
|
|
1cfd3a10ad | ||
|
|
1838029d22 | ||
|
|
7767043a5a | ||
|
|
b1cfa6db19 | ||
|
|
623d5a5546 | ||
|
|
8c676eb965 | ||
|
|
1e4ccce36c | ||
|
|
1808206e74 | ||
|
|
0fbc2b92aa |
@@ -8,7 +8,7 @@ fullnameOverride: ""
|
|||||||
images:
|
images:
|
||||||
owgwui:
|
owgwui:
|
||||||
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
||||||
tag: main
|
tag: v2.9.0
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|||||||
97
package-lock.json
generated
97
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(44)",
|
"version": "2.9.0(23)",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(44)",
|
"version": "2.9.0(23)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.11",
|
"@chakra-ui/icons": "^2.0.11",
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@fontsource/inter": "^4.5.14",
|
"@fontsource/inter": "^4.5.14",
|
||||||
|
"@googlemaps/react-wrapper": "^1.1.35",
|
||||||
|
"@googlemaps/typescript-guards": "^2.0.3",
|
||||||
"@react-spring/web": "^9.5.5",
|
"@react-spring/web": "^9.5.5",
|
||||||
"@tanstack/react-query": "^4.12.0",
|
"@tanstack/react-query": "^4.12.0",
|
||||||
"@textea/json-viewer": "^2.10.0",
|
"@textea/json-viewer": "^2.10.0",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"chakra-react-select": "^4.3.0",
|
"chakra-react-select": "^4.3.0",
|
||||||
"chart.js": "^3.9.1",
|
"chart.js": "^3.9.1",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
|
"fast-equals": "^4.0.3",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"framer-motion": "^7.6.1",
|
"framer-motion": "^7.6.1",
|
||||||
"i18next": "^22.0.0",
|
"i18next": "^22.0.0",
|
||||||
@@ -54,6 +57,7 @@
|
|||||||
"zustand": "^4.1.2"
|
"zustand": "^4.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/google.maps": "^3.51.0",
|
||||||
"@types/node": "^18.11.2",
|
"@types/node": "^18.11.2",
|
||||||
"@types/react": "^18.0.21",
|
"@types/react": "^18.0.21",
|
||||||
"@types/react-csv": "^1.1.3",
|
"@types/react-csv": "^1.1.3",
|
||||||
@@ -2848,6 +2852,30 @@
|
|||||||
"version": "4.5.14",
|
"version": "4.5.14",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@googlemaps/js-api-loader": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-AsnEgNsB7S/VdrHGEQUaUM2e5tmjFGKBAfzR/AqO8O7TPq/jQGvoRw5liPBw4EMF38RDsHmKDV89q/X+qiUREQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@googlemaps/react-wrapper": {
|
||||||
|
"version": "1.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/@googlemaps/react-wrapper/-/react-wrapper-1.1.35.tgz",
|
||||||
|
"integrity": "sha512-vK+BDQMHN0Oqr66cW3ZPWVK43BUmJJBu6P8T74tc6/fKpUJUlFEaZsupgIIRRRDW9ejB8uGagUmwOnA2gdcvbw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@googlemaps/js-api-loader": "^1.13.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@googlemaps/typescript-guards": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw=="
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.10.7",
|
"version": "0.10.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3501,6 +3529,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/google.maps": {
|
||||||
|
"version": "3.51.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.51.0.tgz",
|
||||||
|
"integrity": "sha512-44/oQYjc5D6kxBcI3Qk9rk3IIOMwnlEMWDV7pwPJ2YI89s5Q1OzDrFvR7QJ3LFrpVXEhig+gyagFg54+foinFg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5529,7 +5563,6 @@
|
|||||||
},
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-diff": {
|
"node_modules/fast-diff": {
|
||||||
@@ -5537,6 +5570,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.2.12",
|
"version": "3.2.12",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6642,8 +6680,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
},
|
},
|
||||||
@@ -8853,9 +8892,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
},
|
},
|
||||||
@@ -11425,6 +11465,27 @@
|
|||||||
"@fontsource/inter": {
|
"@fontsource/inter": {
|
||||||
"version": "4.5.14"
|
"version": "4.5.14"
|
||||||
},
|
},
|
||||||
|
"@googlemaps/js-api-loader": {
|
||||||
|
"version": "1.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.15.1.tgz",
|
||||||
|
"integrity": "sha512-AsnEgNsB7S/VdrHGEQUaUM2e5tmjFGKBAfzR/AqO8O7TPq/jQGvoRw5liPBw4EMF38RDsHmKDV89q/X+qiUREQ==",
|
||||||
|
"requires": {
|
||||||
|
"fast-deep-equal": "^3.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@googlemaps/react-wrapper": {
|
||||||
|
"version": "1.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/@googlemaps/react-wrapper/-/react-wrapper-1.1.35.tgz",
|
||||||
|
"integrity": "sha512-vK+BDQMHN0Oqr66cW3ZPWVK43BUmJJBu6P8T74tc6/fKpUJUlFEaZsupgIIRRRDW9ejB8uGagUmwOnA2gdcvbw==",
|
||||||
|
"requires": {
|
||||||
|
"@googlemaps/js-api-loader": "^1.13.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@googlemaps/typescript-guards": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw=="
|
||||||
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.10.7",
|
"version": "0.10.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -11786,6 +11847,12 @@
|
|||||||
"version": "0.0.39",
|
"version": "0.0.39",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/google.maps": {
|
||||||
|
"version": "3.51.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.51.0.tgz",
|
||||||
|
"integrity": "sha512-44/oQYjc5D6kxBcI3Qk9rk3IIOMwnlEMWDV7pwPJ2YI89s5Q1OzDrFvR7QJ3LFrpVXEhig+gyagFg54+foinFg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.11",
|
"version": "7.0.11",
|
||||||
"dev": true
|
"dev": true
|
||||||
@@ -13025,13 +13092,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fast-deep-equal": {
|
"fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3"
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"fast-diff": {
|
"fast-diff": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"fast-equals": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
|
||||||
|
},
|
||||||
"fast-glob": {
|
"fast-glob": {
|
||||||
"version": "3.2.12",
|
"version": "3.2.12",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -13688,7 +13759,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.1"
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
||||||
},
|
},
|
||||||
"jsonfile": {
|
"jsonfile": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
@@ -14981,7 +15054,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(44)",
|
"version": "2.9.0(23)",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.tsx",
|
"main": "index.tsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"format": "prettier --write \"src/**/*.js\"",
|
"format": "prettier --write \"src/**/*x.{ts,tsx,js,jsx}\"",
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
|
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
|
||||||
"clean": "rm -rf node_modules && rm -rf build"
|
"clean": "rm -rf node_modules && rm -rf build"
|
||||||
@@ -22,12 +22,15 @@
|
|||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.4",
|
||||||
"@fontsource/inter": "^4.5.14",
|
"@fontsource/inter": "^4.5.14",
|
||||||
|
"@googlemaps/react-wrapper": "^1.1.35",
|
||||||
|
"@googlemaps/typescript-guards": "^2.0.3",
|
||||||
"@react-spring/web": "^9.5.5",
|
"@react-spring/web": "^9.5.5",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.1.3",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"chakra-react-select": "^4.3.0",
|
"chakra-react-select": "^4.3.0",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
|
"fast-equals": "^4.0.3",
|
||||||
"framer-motion": "^7.6.1",
|
"framer-motion": "^7.6.1",
|
||||||
"i18next": "^22.0.0",
|
"i18next": "^22.0.0",
|
||||||
"i18next-browser-languagedetector": "^6.1.8",
|
"i18next-browser-languagedetector": "^6.1.8",
|
||||||
@@ -60,6 +63,7 @@
|
|||||||
"zustand": "^4.1.2"
|
"zustand": "^4.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/google.maps": "^3.51.0",
|
||||||
"@types/node": "^18.11.2",
|
"@types/node": "^18.11.2",
|
||||||
"@types/react": "^18.0.21",
|
"@types/react": "^18.0.21",
|
||||||
"@types/react-csv": "^1.1.3",
|
"@types/react-csv": "^1.1.3",
|
||||||
|
|||||||
@@ -79,8 +79,11 @@
|
|||||||
"live_view_help": "Hilfe zur Live-Ansicht",
|
"live_view_help": "Hilfe zur Live-Ansicht",
|
||||||
"memory": "Erinnerung",
|
"memory": "Erinnerung",
|
||||||
"memory_used": "Verwendeter Speicher",
|
"memory_used": "Verwendeter Speicher",
|
||||||
"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu",
|
"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten",
|
||||||
"mode": "Modus",
|
"mode": "Modus",
|
||||||
|
"monitoring": "Überwachung",
|
||||||
|
"no_board": "Keine Überwachung",
|
||||||
|
"no_board_description": "Sie überwachen diesen Veranstaltungsort derzeit nicht, klicken Sie hier, um zu beginnen",
|
||||||
"noise": "Lärm",
|
"noise": "Lärm",
|
||||||
"packets": "Pakete",
|
"packets": "Pakete",
|
||||||
"radio": "RADIO",
|
"radio": "RADIO",
|
||||||
@@ -91,6 +94,8 @@
|
|||||||
"retries": "Wiederholungen",
|
"retries": "Wiederholungen",
|
||||||
"search_serials": "Zeitschriften suchen",
|
"search_serials": "Zeitschriften suchen",
|
||||||
"stop_monitoring": "Beenden Sie die Überwachung",
|
"stop_monitoring": "Beenden Sie die Überwachung",
|
||||||
|
"stop_monitoring_success": "Überwachungsort gestoppt!",
|
||||||
|
"stop_monitoring_warning": "Bist du sicher? Dadurch werden alle aufgezeichneten Überwachungsdaten für diesen Veranstaltungsort gelöscht",
|
||||||
"temperature": "Temperatur",
|
"temperature": "Temperatur",
|
||||||
"title": "ANALYTICS",
|
"title": "ANALYTICS",
|
||||||
"total_data": "Gesamtdaten",
|
"total_data": "Gesamtdaten",
|
||||||
@@ -175,6 +180,7 @@
|
|||||||
"other": "Befehle",
|
"other": "Befehle",
|
||||||
"override_dfs": "DFS überschreiben",
|
"override_dfs": "DFS überschreiben",
|
||||||
"reboot": "Starten Sie neu",
|
"reboot": "Starten Sie neu",
|
||||||
|
"reboot_description": "Möchten Sie dieses Gerät neu starten?",
|
||||||
"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}",
|
"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}",
|
||||||
"reboot_success": "Neustartbefehl erfolgreich gesendet!",
|
"reboot_success": "Neustartbefehl erfolgreich gesendet!",
|
||||||
"revision": "Revision",
|
"revision": "Revision",
|
||||||
@@ -391,6 +397,7 @@
|
|||||||
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
|
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
|
||||||
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
|
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
|
||||||
"weight": "Gewicht",
|
"weight": "Gewicht",
|
||||||
|
"wifi_bands_max": "Es können nicht mehr als 8 SSIDs dieses WLAN-Band verwenden",
|
||||||
"wifi_frames": "WiFi-Frames"
|
"wifi_frames": "WiFi-Frames"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -600,6 +607,7 @@
|
|||||||
"certificate_expires_in": "Zertifikat läuft ab in",
|
"certificate_expires_in": "Zertifikat läuft ab in",
|
||||||
"certificate_expiry": "Zert. Läuft ab in",
|
"certificate_expiry": "Zert. Läuft ab in",
|
||||||
"connected": "In Verbindung gebracht",
|
"connected": "In Verbindung gebracht",
|
||||||
|
"crash_logs": "Absturzprotokolle",
|
||||||
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
|
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
|
||||||
"create_success": " Geräte erfolgreich erstellt",
|
"create_success": " Geräte erfolgreich erstellt",
|
||||||
"current_firmware": "Aktuelle Firmware",
|
"current_firmware": "Aktuelle Firmware",
|
||||||
@@ -613,6 +621,7 @@
|
|||||||
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
|
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
|
||||||
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
|
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
|
||||||
|
"logs_one": "Log",
|
||||||
"new_devices": "Neue Geräte",
|
"new_devices": "Neue Geräte",
|
||||||
"no_model_image": "Kein Modellbild gefunden",
|
"no_model_image": "Kein Modellbild gefunden",
|
||||||
"not_connected": "Nicht verbunden",
|
"not_connected": "Nicht verbunden",
|
||||||
@@ -621,6 +630,8 @@
|
|||||||
"one": "Gerät",
|
"one": "Gerät",
|
||||||
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
|
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
|
||||||
"restricted": "Beschränkt",
|
"restricted": "Beschränkt",
|
||||||
|
"restricted_overriden": "Dies ist ein eingeschränktes Gerät, aber es befindet sich im Entwicklungsmodus. Alle Einschränkungen werden derzeit ignoriert",
|
||||||
|
"restrictions_overriden_title": "Dev-Modus",
|
||||||
"sanity": "Gesundheit",
|
"sanity": "Gesundheit",
|
||||||
"start_import": "Geräteimport starten",
|
"start_import": "Geräteimport starten",
|
||||||
"test_batch": "Testen Sie Importdaten",
|
"test_batch": "Testen Sie Importdaten",
|
||||||
@@ -671,7 +682,15 @@
|
|||||||
"test_digicert_creds": "Anmeldeinformationen testen",
|
"test_digicert_creds": "Anmeldeinformationen testen",
|
||||||
"title": "Entitäten",
|
"title": "Entitäten",
|
||||||
"tree": "Entitätsbaum",
|
"tree": "Entitätsbaum",
|
||||||
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden. Bitte erstellen Sie neue Entitäten und erstellen Sie Veranstaltungsorte unter diesen."
|
"update_success": "Entität aktualisiert!",
|
||||||
|
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
|
||||||
|
"last_db_update_modal": "Firmware-Datenbank",
|
||||||
|
"last_db_update_title": "Datenbank",
|
||||||
|
"start_db_update": "Datenbankaktualisierung starten",
|
||||||
|
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Unterstützt von",
|
"powered_by": "Unterstützt von",
|
||||||
@@ -765,13 +784,17 @@
|
|||||||
"city": "Stadt",
|
"city": "Stadt",
|
||||||
"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden",
|
"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden",
|
||||||
"country": "Land",
|
"country": "Land",
|
||||||
|
"elevation": "Elevation",
|
||||||
"geocode": "Geo-Code",
|
"geocode": "Geo-Code",
|
||||||
|
"lat": "Breite",
|
||||||
|
"longitude": "Längengrad",
|
||||||
"one": "Ort",
|
"one": "Ort",
|
||||||
"other": "Standorte",
|
"other": "Standorte",
|
||||||
"postal": "Postleitzahl",
|
"postal": "Postleitzahl",
|
||||||
"state": "Bundesstaat / Provinz",
|
"state": "Bundesstaat / Provinz",
|
||||||
"title": "Standorte",
|
"title": "Standorte",
|
||||||
"to_claim": "Standorte zu beanspruchen"
|
"to_claim": "Standorte zu beanspruchen",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Zugangsrichtlinien",
|
"access_policy": "Zugangsrichtlinien",
|
||||||
@@ -797,6 +820,7 @@
|
|||||||
"reset_password": "Passwort zurücksetzen",
|
"reset_password": "Passwort zurücksetzen",
|
||||||
"sign_in": "Einloggen",
|
"sign_in": "Einloggen",
|
||||||
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
|
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
|
||||||
|
"suspended_error": "Konto gesperrt, wenden Sie sich bitte an Ihren Administrator",
|
||||||
"verification": "Bestätigen Sie Ihre Anmeldung",
|
"verification": "Bestätigen Sie Ihre Anmeldung",
|
||||||
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
|
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
|
||||||
"welcome_back": "Willkommen zurück!",
|
"welcome_back": "Willkommen zurück!",
|
||||||
@@ -902,7 +926,7 @@
|
|||||||
"dfs": "DFS-Überschreibung",
|
"dfs": "DFS-Überschreibung",
|
||||||
"gw_commands": "Gateway-Befehle",
|
"gw_commands": "Gateway-Befehle",
|
||||||
"identifier": "Identifikator",
|
"identifier": "Identifikator",
|
||||||
"key_verification": "Überprüfung des Signaturschlüssels",
|
"key_verification": "Signieren von Schlüsselinformationen",
|
||||||
"restricted": "Beschränkt",
|
"restricted": "Beschränkt",
|
||||||
"signed_upgrade": "Nur signiertes Upgrade",
|
"signed_upgrade": "Nur signiertes Upgrade",
|
||||||
"title": "Beschränkungen",
|
"title": "Beschränkungen",
|
||||||
@@ -1022,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Back-End-Protokolle",
|
"backend_logs": "Back-End-Protokolle",
|
||||||
|
"configuration": "Aufbau",
|
||||||
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
||||||
"endpoint": "Endpunkt",
|
"endpoint": "Endpunkt",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
@@ -1032,6 +1057,10 @@
|
|||||||
"os": "Betriebssystem",
|
"os": "Betriebssystem",
|
||||||
"processors": "Prozessoren",
|
"processors": "Prozessoren",
|
||||||
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
|
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
|
||||||
|
"secrets": "Geheimnisse",
|
||||||
|
"secrets_create": "Geheimnis erstellen",
|
||||||
|
"secrets_one": "Geheimnis",
|
||||||
|
"services": "dienstleistungen",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"subsystems": "Subsysteme",
|
"subsystems": "Subsysteme",
|
||||||
"success_reload": "Reload-Befehl erfolgreich gesendet!",
|
"success_reload": "Reload-Befehl erfolgreich gesendet!",
|
||||||
@@ -1053,9 +1082,11 @@
|
|||||||
"previous_page": "Vorherige Seite"
|
"previous_page": "Vorherige Seite"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "E-Mail nicht validiert",
|
||||||
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
|
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"role": "Rolle",
|
"role": "Rolle",
|
||||||
|
"suspended": "Suspendiert",
|
||||||
"title": "Nutzer"
|
"title": "Nutzer"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1131,11 @@
|
|||||||
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
|
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
|
||||||
"title": "Veranstaltungsorte",
|
"title": "Veranstaltungsorte",
|
||||||
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
|
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
|
||||||
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
|
"update_success": "Veranstaltungsort aktualisiert!",
|
||||||
|
"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte",
|
||||||
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
|
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
|
||||||
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
|
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
|
||||||
|
"upgrade_options_available": "Hier sind alle verfügbaren Revisionen, bitte wählen Sie diejenige aus, auf die ALLE Geräte dieses Veranstaltungsortes aktualisiert werden sollen",
|
||||||
"use_existing": "Benutze existierendes",
|
"use_existing": "Benutze existierendes",
|
||||||
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
|
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
|
||||||
"use_this_contact": "Verwenden Sie diesen Kontakt"
|
"use_this_contact": "Verwenden Sie diesen Kontakt"
|
||||||
|
|||||||
@@ -79,8 +79,11 @@
|
|||||||
"live_view_help": "Live View Help",
|
"live_view_help": "Live View Help",
|
||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"memory_used": "Memory Used",
|
"memory_used": "Memory Used",
|
||||||
"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu",
|
"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"monitoring": "Monitoring",
|
||||||
|
"no_board": "No Monitoring",
|
||||||
|
"no_board_description": "You are not monitoring this Venue at the moment, click here to start",
|
||||||
"noise": "Noise",
|
"noise": "Noise",
|
||||||
"packets": "Packets",
|
"packets": "Packets",
|
||||||
"radio": "Radio",
|
"radio": "Radio",
|
||||||
@@ -91,6 +94,8 @@
|
|||||||
"retries": "Retries",
|
"retries": "Retries",
|
||||||
"search_serials": "Search Serials",
|
"search_serials": "Search Serials",
|
||||||
"stop_monitoring": "Stop Monitoring",
|
"stop_monitoring": "Stop Monitoring",
|
||||||
|
"stop_monitoring_success": "Stopped Monitoring Venue!",
|
||||||
|
"stop_monitoring_warning": "Are you sure? This will erase all recorded monitoring data for this venue",
|
||||||
"temperature": "Temperature",
|
"temperature": "Temperature",
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"total_data": "Total Data",
|
"total_data": "Total Data",
|
||||||
@@ -175,6 +180,7 @@
|
|||||||
"other": "Commands",
|
"other": "Commands",
|
||||||
"override_dfs": "Override DFS",
|
"override_dfs": "Override DFS",
|
||||||
"reboot": "Reboot",
|
"reboot": "Reboot",
|
||||||
|
"reboot_description": "Do you want to reboot this device?",
|
||||||
"reboot_error": "Error while sending reboot command: {{e}}",
|
"reboot_error": "Error while sending reboot command: {{e}}",
|
||||||
"reboot_success": "Successfully sent reboot command!",
|
"reboot_success": "Successfully sent reboot command!",
|
||||||
"revision": "Revision",
|
"revision": "Revision",
|
||||||
@@ -391,6 +397,7 @@
|
|||||||
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
|
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
|
||||||
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
|
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
|
"wifi_bands_max": "There cannot be more than 8 SSIDs using this wifi-band",
|
||||||
"wifi_frames": "WiFi Frames"
|
"wifi_frames": "WiFi Frames"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -600,6 +607,7 @@
|
|||||||
"certificate_expires_in": "Certificate Expiry",
|
"certificate_expires_in": "Certificate Expiry",
|
||||||
"certificate_expiry": "Cert. Expires In",
|
"certificate_expiry": "Cert. Expires In",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
"crash_logs": "Crash Logs",
|
||||||
"create_errors": "errors while trying to create devices",
|
"create_errors": "errors while trying to create devices",
|
||||||
"create_success": " devices successfully created",
|
"create_success": " devices successfully created",
|
||||||
"current_firmware": "Current Firmware",
|
"current_firmware": "Current Firmware",
|
||||||
@@ -613,6 +621,7 @@
|
|||||||
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
|
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
|
||||||
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
|
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
|
||||||
|
"logs_one": "Log",
|
||||||
"new_devices": "new devices",
|
"new_devices": "new devices",
|
||||||
"no_model_image": "No Model Image Found",
|
"no_model_image": "No Model Image Found",
|
||||||
"not_connected": "Not Connected",
|
"not_connected": "Not Connected",
|
||||||
@@ -621,6 +630,8 @@
|
|||||||
"one": "Device",
|
"one": "Device",
|
||||||
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
|
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
|
||||||
"restricted": "Restricted",
|
"restricted": "Restricted",
|
||||||
|
"restricted_overriden": "This is a restricted device, but it is in development mode. All restrictions are currently ignored",
|
||||||
|
"restrictions_overriden_title": "Dev Mode",
|
||||||
"sanity": "Sanity",
|
"sanity": "Sanity",
|
||||||
"start_import": "Start Device Importation",
|
"start_import": "Start Device Importation",
|
||||||
"test_batch": "Test Import Data",
|
"test_batch": "Test Import Data",
|
||||||
@@ -671,7 +682,15 @@
|
|||||||
"test_digicert_creds": "Test Credentials",
|
"test_digicert_creds": "Test Credentials",
|
||||||
"title": "Entities",
|
"title": "Entities",
|
||||||
"tree": "Entity Tree",
|
"tree": "Entity Tree",
|
||||||
"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these."
|
"update_success": "Entity updated!",
|
||||||
|
"venues_under_root": "Venues cannot be created directly under the root entity"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
|
||||||
|
"last_db_update_modal": "Firmware Database",
|
||||||
|
"last_db_update_title": "Database",
|
||||||
|
"start_db_update": "Start Database Update",
|
||||||
|
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Powered By",
|
"powered_by": "Powered By",
|
||||||
@@ -765,13 +784,17 @@
|
|||||||
"city": "City",
|
"city": "City",
|
||||||
"claim_explanation": "To claim locations you can use the table below",
|
"claim_explanation": "To claim locations you can use the table below",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
|
"elevation": "Elevation",
|
||||||
"geocode": "Geo Code",
|
"geocode": "Geo Code",
|
||||||
|
"lat": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
"one": "Location",
|
"one": "Location",
|
||||||
"other": "Locations",
|
"other": "Locations",
|
||||||
"postal": "ZIP/Postal Code",
|
"postal": "ZIP/Postal Code",
|
||||||
"state": "State/Province",
|
"state": "State/Province",
|
||||||
"title": "Locations",
|
"title": "Locations",
|
||||||
"to_claim": "Locations to claim"
|
"to_claim": "Locations to claim",
|
||||||
|
"view_gps": "View GPS Location"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Access Policy",
|
"access_policy": "Access Policy",
|
||||||
@@ -797,6 +820,7 @@
|
|||||||
"reset_password": "Reset Password",
|
"reset_password": "Reset Password",
|
||||||
"sign_in": "Sign In",
|
"sign_in": "Sign In",
|
||||||
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
|
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
|
||||||
|
"suspended_error": "Suspended account, please contact your administrator",
|
||||||
"verification": "Verify your login",
|
"verification": "Verify your login",
|
||||||
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
|
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
|
||||||
"welcome_back": "Welcome Back!",
|
"welcome_back": "Welcome Back!",
|
||||||
@@ -902,7 +926,7 @@
|
|||||||
"dfs": "DFS Override",
|
"dfs": "DFS Override",
|
||||||
"gw_commands": "Gateway Commands",
|
"gw_commands": "Gateway Commands",
|
||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"key_verification": "Signing Key Verification",
|
"key_verification": "Signing Key Information",
|
||||||
"restricted": "Restricted",
|
"restricted": "Restricted",
|
||||||
"signed_upgrade": "Signed Upgrade Only",
|
"signed_upgrade": "Signed Upgrade Only",
|
||||||
"title": "Restrictions",
|
"title": "Restrictions",
|
||||||
@@ -1022,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Back-End Logs",
|
"backend_logs": "Back-End Logs",
|
||||||
|
"configuration": "Configuration",
|
||||||
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
|
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"hostname": "Host Name",
|
"hostname": "Host Name",
|
||||||
@@ -1032,6 +1057,10 @@
|
|||||||
"os": "Operating System",
|
"os": "Operating System",
|
||||||
"processors": "Processors",
|
"processors": "Processors",
|
||||||
"reload_chosen_subsystems": "Reload Chosen Subsystems",
|
"reload_chosen_subsystems": "Reload Chosen Subsystems",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"secrets_create": "Create Secret",
|
||||||
|
"secrets_one": "Secret",
|
||||||
|
"services": "Services",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"subsystems": "Subsystems",
|
"subsystems": "Subsystems",
|
||||||
"success_reload": "Successfully sent reload command!",
|
"success_reload": "Successfully sent reload command!",
|
||||||
@@ -1053,9 +1082,11 @@
|
|||||||
"previous_page": "Previous Page"
|
"previous_page": "Previous Page"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "email not validated",
|
||||||
"error_fetching": "Error fetching user information: {{e}}",
|
"error_fetching": "Error fetching user information: {{e}}",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
|
"suspended": "suspended",
|
||||||
"title": "User"
|
"title": "User"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1131,11 @@
|
|||||||
"successfully_update_devices": "Updating {{num}} devices!",
|
"successfully_update_devices": "Updating {{num}} devices!",
|
||||||
"title": "Venues",
|
"title": "Venues",
|
||||||
"update_all_devices": "Update All Device Configurations",
|
"update_all_devices": "Update All Device Configurations",
|
||||||
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
|
"update_success": "Venue updated!",
|
||||||
|
"upgrade_all_devices": "Upgrade All Devices Firmware",
|
||||||
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
|
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
|
||||||
"upgrade_all_devices_success": "Successfully started upgrading devices!",
|
"upgrade_all_devices_success": "Successfully started upgrading devices!",
|
||||||
|
"upgrade_options_available": "Here are all available revisions, please select the one you want ALL of this venue's devices to be upgrade to",
|
||||||
"use_existing": "Use Existing",
|
"use_existing": "Use Existing",
|
||||||
"use_existing_contacts": "Use Existing Contacts",
|
"use_existing_contacts": "Use Existing Contacts",
|
||||||
"use_this_contact": "Use this contact"
|
"use_this_contact": "Use this contact"
|
||||||
|
|||||||
@@ -79,8 +79,11 @@
|
|||||||
"live_view_help": "Ayuda de visualización en vivo",
|
"live_view_help": "Ayuda de visualización en vivo",
|
||||||
"memory": "Memoria",
|
"memory": "Memoria",
|
||||||
"memory_used": "Memoria usada",
|
"memory_used": "Memoria usada",
|
||||||
"missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior",
|
"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
|
"monitoring": "Vigilancia",
|
||||||
|
"no_board": "Sin monitoreo",
|
||||||
|
"no_board_description": "No está monitoreando este lugar en este momento, haga clic aquí para comenzar",
|
||||||
"noise": "Ruido",
|
"noise": "Ruido",
|
||||||
"packets": "Paquetes",
|
"packets": "Paquetes",
|
||||||
"radio": "RADIO",
|
"radio": "RADIO",
|
||||||
@@ -91,6 +94,8 @@
|
|||||||
"retries": "Reintentos",
|
"retries": "Reintentos",
|
||||||
"search_serials": "Buscar seriales",
|
"search_serials": "Buscar seriales",
|
||||||
"stop_monitoring": "Dejar de monitorear",
|
"stop_monitoring": "Dejar de monitorear",
|
||||||
|
"stop_monitoring_success": "¡Se detuvo el lugar de monitoreo!",
|
||||||
|
"stop_monitoring_warning": "¿Está seguro? Esto borrará todos los datos de monitoreo grabados para este lugar.",
|
||||||
"temperature": "temperatura",
|
"temperature": "temperatura",
|
||||||
"title": "ANALÍTICA",
|
"title": "ANALÍTICA",
|
||||||
"total_data": "Datos totales",
|
"total_data": "Datos totales",
|
||||||
@@ -175,6 +180,7 @@
|
|||||||
"other": "comandos",
|
"other": "comandos",
|
||||||
"override_dfs": "Anular DFS",
|
"override_dfs": "Anular DFS",
|
||||||
"reboot": "Reiniciar",
|
"reboot": "Reiniciar",
|
||||||
|
"reboot_description": "¿Quieres reiniciar este dispositivo?",
|
||||||
"reboot_error": "Error al enviar el comando de reinicio: {{e}}",
|
"reboot_error": "Error al enviar el comando de reinicio: {{e}}",
|
||||||
"reboot_success": "¡Comando de reinicio enviado con éxito!",
|
"reboot_success": "¡Comando de reinicio enviado con éxito!",
|
||||||
"revision": "revisión",
|
"revision": "revisión",
|
||||||
@@ -391,6 +397,7 @@
|
|||||||
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
|
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
|
||||||
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
|
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
|
"wifi_bands_max": "No puede haber más de 8 SSID usando esta banda wifi",
|
||||||
"wifi_frames": "Marcos WiFi"
|
"wifi_frames": "Marcos WiFi"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -600,6 +607,7 @@
|
|||||||
"certificate_expires_in": "El certificado caduca en",
|
"certificate_expires_in": "El certificado caduca en",
|
||||||
"certificate_expiry": "Cert. Expira en",
|
"certificate_expiry": "Cert. Expira en",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"crash_logs": "Registros de fallas",
|
||||||
"create_errors": "errores al intentar crear dispositivos",
|
"create_errors": "errores al intentar crear dispositivos",
|
||||||
"create_success": " dispositivos creados con éxito",
|
"create_success": " dispositivos creados con éxito",
|
||||||
"current_firmware": "Firmware actual",
|
"current_firmware": "Firmware actual",
|
||||||
@@ -613,6 +621,7 @@
|
|||||||
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
|
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
|
||||||
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
|
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
|
||||||
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
|
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
|
||||||
|
"logs_one": "Iniciar sesión",
|
||||||
"new_devices": "Nuevos dispositivos",
|
"new_devices": "Nuevos dispositivos",
|
||||||
"no_model_image": "No se encontró ninguna imagen de modelo",
|
"no_model_image": "No se encontró ninguna imagen de modelo",
|
||||||
"not_connected": "No conectado",
|
"not_connected": "No conectado",
|
||||||
@@ -621,6 +630,8 @@
|
|||||||
"one": "Dispositivo",
|
"one": "Dispositivo",
|
||||||
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
|
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
|
||||||
"restricted": "Restringido",
|
"restricted": "Restringido",
|
||||||
|
"restricted_overriden": "Este es un dispositivo restringido, pero está en modo de desarrollo. Actualmente se ignoran todas las restricciones.",
|
||||||
|
"restrictions_overriden_title": "MODO DE DESARROLLO",
|
||||||
"sanity": "Cordura",
|
"sanity": "Cordura",
|
||||||
"start_import": "Iniciar la importación de dispositivos",
|
"start_import": "Iniciar la importación de dispositivos",
|
||||||
"test_batch": "Datos de importación de prueba",
|
"test_batch": "Datos de importación de prueba",
|
||||||
@@ -671,7 +682,15 @@
|
|||||||
"test_digicert_creds": "Credenciales de prueba",
|
"test_digicert_creds": "Credenciales de prueba",
|
||||||
"title": "entidades",
|
"title": "entidades",
|
||||||
"tree": "Árbol de entidades",
|
"tree": "Árbol de entidades",
|
||||||
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas."
|
"update_success": "¡Entidad actualizada!",
|
||||||
|
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
|
||||||
|
"last_db_update_modal": "Base de datos de firmware",
|
||||||
|
"last_db_update_title": "Base de datos",
|
||||||
|
"start_db_update": "Iniciar actualización de la base de datos",
|
||||||
|
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "energizado por",
|
"powered_by": "energizado por",
|
||||||
@@ -765,13 +784,17 @@
|
|||||||
"city": "ciudad",
|
"city": "ciudad",
|
||||||
"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación",
|
"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación",
|
||||||
"country": "País",
|
"country": "País",
|
||||||
|
"elevation": "Elevación",
|
||||||
"geocode": "Código geográfico",
|
"geocode": "Código geográfico",
|
||||||
|
"lat": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
"one": "Ubicación",
|
"one": "Ubicación",
|
||||||
"other": "Ubicaciones",
|
"other": "Ubicaciones",
|
||||||
"postal": "código postal",
|
"postal": "código postal",
|
||||||
"state": "Provincia del estado",
|
"state": "Provincia del estado",
|
||||||
"title": "Ubicaciones",
|
"title": "Ubicaciones",
|
||||||
"to_claim": "Ubicaciones para reclamar"
|
"to_claim": "Ubicaciones para reclamar",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Política de acceso",
|
"access_policy": "Política de acceso",
|
||||||
@@ -797,6 +820,7 @@
|
|||||||
"reset_password": "Restablecer la contraseña",
|
"reset_password": "Restablecer la contraseña",
|
||||||
"sign_in": "Registrarse",
|
"sign_in": "Registrarse",
|
||||||
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
|
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
|
||||||
|
"suspended_error": "Cuenta suspendida, comuníquese con su administrador",
|
||||||
"verification": "Verifica tu inicio de sesión",
|
"verification": "Verifica tu inicio de sesión",
|
||||||
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
|
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
|
||||||
"welcome_back": "¡Dar una buena acogida!",
|
"welcome_back": "¡Dar una buena acogida!",
|
||||||
@@ -902,7 +926,7 @@
|
|||||||
"dfs": "Anulación de DFS",
|
"dfs": "Anulación de DFS",
|
||||||
"gw_commands": "Comandos de puerta de enlace",
|
"gw_commands": "Comandos de puerta de enlace",
|
||||||
"identifier": "Identificador",
|
"identifier": "Identificador",
|
||||||
"key_verification": "Verificación de clave de firma",
|
"key_verification": "Información clave de firma",
|
||||||
"restricted": "Restringido",
|
"restricted": "Restringido",
|
||||||
"signed_upgrade": "Solo actualización firmada",
|
"signed_upgrade": "Solo actualización firmada",
|
||||||
"title": "Las restricciones",
|
"title": "Las restricciones",
|
||||||
@@ -1022,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Registros de back-end",
|
"backend_logs": "Registros de back-end",
|
||||||
|
"configuration": "Configuración",
|
||||||
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
|
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
|
||||||
"endpoint": "punto final",
|
"endpoint": "punto final",
|
||||||
"hostname": "Nombre de host",
|
"hostname": "Nombre de host",
|
||||||
@@ -1032,6 +1057,10 @@
|
|||||||
"os": "sistema operativo",
|
"os": "sistema operativo",
|
||||||
"processors": "Procesadores",
|
"processors": "Procesadores",
|
||||||
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
|
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
|
||||||
|
"secrets": "Misterios",
|
||||||
|
"secrets_create": "Crear secreto",
|
||||||
|
"secrets_one": "secreto",
|
||||||
|
"services": "Servicios",
|
||||||
"start": "comienzo",
|
"start": "comienzo",
|
||||||
"subsystems": "Subsistemas",
|
"subsystems": "Subsistemas",
|
||||||
"success_reload": "¡Comando de recarga enviado con éxito!",
|
"success_reload": "¡Comando de recarga enviado con éxito!",
|
||||||
@@ -1053,9 +1082,11 @@
|
|||||||
"previous_page": "Página anterior"
|
"previous_page": "Página anterior"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "correo electrónico no validado",
|
||||||
"error_fetching": "Error al obtener la información del usuario: {{e}}",
|
"error_fetching": "Error al obtener la información del usuario: {{e}}",
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"role": "papel",
|
"role": "papel",
|
||||||
|
"suspended": "Suspendido",
|
||||||
"title": "Usuario"
|
"title": "Usuario"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1131,11 @@
|
|||||||
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
|
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
|
||||||
"title": "Sedes",
|
"title": "Sedes",
|
||||||
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
|
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
|
||||||
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
|
"update_success": "Lugar actualizado!",
|
||||||
|
"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos",
|
||||||
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
|
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
|
||||||
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
|
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
|
||||||
|
"upgrade_options_available": "Aquí están todas las revisiones disponibles, seleccione la que desea que TODOS los dispositivos de este lugar se actualicen",
|
||||||
"use_existing": "Utilizar existente",
|
"use_existing": "Utilizar existente",
|
||||||
"use_existing_contacts": "Usar contactos existentes",
|
"use_existing_contacts": "Usar contactos existentes",
|
||||||
"use_this_contact": "Usa este contacto"
|
"use_this_contact": "Usa este contacto"
|
||||||
|
|||||||
@@ -79,8 +79,11 @@
|
|||||||
"live_view_help": "Aide sur l'affichage en direct",
|
"live_view_help": "Aide sur l'affichage en direct",
|
||||||
"memory": "mémoire",
|
"memory": "mémoire",
|
||||||
"memory_used": "Mémoire utilisée",
|
"memory_used": "Mémoire utilisée",
|
||||||
"missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut",
|
"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"monitoring": "surveillance",
|
||||||
|
"no_board": "Aucune surveillance",
|
||||||
|
"no_board_description": "Vous ne surveillez pas ce lieu pour le moment, cliquez ici pour commencer",
|
||||||
"noise": "Bruit",
|
"noise": "Bruit",
|
||||||
"packets": "Paquets",
|
"packets": "Paquets",
|
||||||
"radio": "Radio",
|
"radio": "Radio",
|
||||||
@@ -91,6 +94,8 @@
|
|||||||
"retries": "Tentatives",
|
"retries": "Tentatives",
|
||||||
"search_serials": "Rechercher des publications en série",
|
"search_serials": "Rechercher des publications en série",
|
||||||
"stop_monitoring": "Arrêter la surveillance",
|
"stop_monitoring": "Arrêter la surveillance",
|
||||||
|
"stop_monitoring_success": "Lieu de surveillance arrêté !",
|
||||||
|
"stop_monitoring_warning": "Êtes-vous sûr? Cela effacera toutes les données de surveillance enregistrées pour ce lieu",
|
||||||
"temperature": "Température",
|
"temperature": "Température",
|
||||||
"title": "ANALYTIQUE",
|
"title": "ANALYTIQUE",
|
||||||
"total_data": "Données totales",
|
"total_data": "Données totales",
|
||||||
@@ -175,6 +180,7 @@
|
|||||||
"other": "Les commandes",
|
"other": "Les commandes",
|
||||||
"override_dfs": "Remplacer DFS",
|
"override_dfs": "Remplacer DFS",
|
||||||
"reboot": "Redémarrer",
|
"reboot": "Redémarrer",
|
||||||
|
"reboot_description": "Voulez-vous redémarrer cet appareil ?",
|
||||||
"reboot_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}",
|
"reboot_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}",
|
||||||
"reboot_success": "Commande de redémarrage envoyée avec succès !",
|
"reboot_success": "Commande de redémarrage envoyée avec succès !",
|
||||||
"revision": "Révision",
|
"revision": "Révision",
|
||||||
@@ -391,6 +397,7 @@
|
|||||||
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
|
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
|
||||||
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
|
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
|
||||||
"weight": "Poids",
|
"weight": "Poids",
|
||||||
|
"wifi_bands_max": "Il ne peut y avoir plus de 8 SSID utilisant cette bande wifi",
|
||||||
"wifi_frames": "Cadres Wi-Fi"
|
"wifi_frames": "Cadres Wi-Fi"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -600,6 +607,7 @@
|
|||||||
"certificate_expires_in": "Le certificat expire dans",
|
"certificate_expires_in": "Le certificat expire dans",
|
||||||
"certificate_expiry": "Cert. Expire dans",
|
"certificate_expiry": "Cert. Expire dans",
|
||||||
"connected": "Connecté",
|
"connected": "Connecté",
|
||||||
|
"crash_logs": "Journaux des plantages",
|
||||||
"create_errors": "erreurs lors de la tentative de création d'appareils",
|
"create_errors": "erreurs lors de la tentative de création d'appareils",
|
||||||
"create_success": " appareils créés avec succès",
|
"create_success": " appareils créés avec succès",
|
||||||
"current_firmware": "Firmware actuel",
|
"current_firmware": "Firmware actuel",
|
||||||
@@ -613,6 +621,7 @@
|
|||||||
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
|
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
|
||||||
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
|
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
|
||||||
|
"logs_one": "Bûche",
|
||||||
"new_devices": "nouveaux appareils",
|
"new_devices": "nouveaux appareils",
|
||||||
"no_model_image": "Aucune image de modèle trouvée",
|
"no_model_image": "Aucune image de modèle trouvée",
|
||||||
"not_connected": "Pas connecté",
|
"not_connected": "Pas connecté",
|
||||||
@@ -621,6 +630,8 @@
|
|||||||
"one": "Dispositif",
|
"one": "Dispositif",
|
||||||
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
|
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
|
||||||
"restricted": "Limité",
|
"restricted": "Limité",
|
||||||
|
"restricted_overriden": "Il s'agit d'un appareil restreint, mais il est en mode développement. Toutes les restrictions sont actuellement ignorées",
|
||||||
|
"restrictions_overriden_title": "Mode développement",
|
||||||
"sanity": "Santé mentale",
|
"sanity": "Santé mentale",
|
||||||
"start_import": "Démarrer l'importation de l'appareil",
|
"start_import": "Démarrer l'importation de l'appareil",
|
||||||
"test_batch": "Tester les données d'importation",
|
"test_batch": "Tester les données d'importation",
|
||||||
@@ -671,7 +682,15 @@
|
|||||||
"test_digicert_creds": "Tester les informations d'identification",
|
"test_digicert_creds": "Tester les informations d'identification",
|
||||||
"title": "Entités",
|
"title": "Entités",
|
||||||
"tree": "Arborescence des entités",
|
"tree": "Arborescence des entités",
|
||||||
"venues_under_root": "Les sites ne peuvent pas être créés directement sous l'entité racine. Veuillez créer de nouvelles entités et créer des lieux sous celles-ci."
|
"update_success": "Entité mise à jour !",
|
||||||
|
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
|
||||||
|
"last_db_update_modal": "Base de données du micrologiciel",
|
||||||
|
"last_db_update_title": "Base de données",
|
||||||
|
"start_db_update": "Démarrer la mise à jour de la base de données",
|
||||||
|
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Alimenté par",
|
"powered_by": "Alimenté par",
|
||||||
@@ -765,13 +784,17 @@
|
|||||||
"city": "Ville",
|
"city": "Ville",
|
||||||
"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous",
|
"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous",
|
||||||
"country": "Pays",
|
"country": "Pays",
|
||||||
|
"elevation": "Élévation",
|
||||||
"geocode": "Geo code",
|
"geocode": "Geo code",
|
||||||
|
"lat": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
"one": "Emplacement",
|
"one": "Emplacement",
|
||||||
"other": "Emplacements",
|
"other": "Emplacements",
|
||||||
"postal": "Zip / code postal",
|
"postal": "Zip / code postal",
|
||||||
"state": "Etat / Province",
|
"state": "Etat / Province",
|
||||||
"title": "Emplacements",
|
"title": "Emplacements",
|
||||||
"to_claim": "Emplacements à réclamer"
|
"to_claim": "Emplacements à réclamer",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Politique d'accès",
|
"access_policy": "Politique d'accès",
|
||||||
@@ -797,6 +820,7 @@
|
|||||||
"reset_password": "Réinitialiser le mot de passe",
|
"reset_password": "Réinitialiser le mot de passe",
|
||||||
"sign_in": "se connecter",
|
"sign_in": "se connecter",
|
||||||
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
|
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
|
||||||
|
"suspended_error": "Compte suspendu, veuillez contacter votre administrateur",
|
||||||
"verification": "Vérifiez votre connexion",
|
"verification": "Vérifiez votre connexion",
|
||||||
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
|
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
|
||||||
"welcome_back": "Nous saluons le retour!",
|
"welcome_back": "Nous saluons le retour!",
|
||||||
@@ -902,7 +926,7 @@
|
|||||||
"dfs": "Remplacement DFS",
|
"dfs": "Remplacement DFS",
|
||||||
"gw_commands": "Commandes de passerelle",
|
"gw_commands": "Commandes de passerelle",
|
||||||
"identifier": "Identifiant",
|
"identifier": "Identifiant",
|
||||||
"key_verification": "Vérification de la clé de signature",
|
"key_verification": "Signature des informations clés",
|
||||||
"restricted": "Limité",
|
"restricted": "Limité",
|
||||||
"signed_upgrade": "Mise à niveau signée uniquement",
|
"signed_upgrade": "Mise à niveau signée uniquement",
|
||||||
"title": "Restrictions",
|
"title": "Restrictions",
|
||||||
@@ -1022,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Journaux principaux",
|
"backend_logs": "Journaux principaux",
|
||||||
|
"configuration": "Configuration",
|
||||||
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
|
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
|
||||||
"endpoint": "Point final",
|
"endpoint": "Point final",
|
||||||
"hostname": "nom d'hôte",
|
"hostname": "nom d'hôte",
|
||||||
@@ -1032,6 +1057,10 @@
|
|||||||
"os": "Système opérateur",
|
"os": "Système opérateur",
|
||||||
"processors": "Processeurs",
|
"processors": "Processeurs",
|
||||||
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
|
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"secrets_create": "Créer un secret",
|
||||||
|
"secrets_one": "Secret",
|
||||||
|
"services": "Prestations de service",
|
||||||
"start": "Début",
|
"start": "Début",
|
||||||
"subsystems": "Sous-systèmes",
|
"subsystems": "Sous-systèmes",
|
||||||
"success_reload": "Commande de rechargement envoyée avec succès !",
|
"success_reload": "Commande de rechargement envoyée avec succès !",
|
||||||
@@ -1053,9 +1082,11 @@
|
|||||||
"previous_page": "Page précédente"
|
"previous_page": "Page précédente"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "Mail non valide",
|
||||||
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
|
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"role": "Rôle",
|
"role": "Rôle",
|
||||||
|
"suspended": "Suspendu",
|
||||||
"title": "Utilisateur"
|
"title": "Utilisateur"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1131,11 @@
|
|||||||
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
|
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
|
||||||
"title": "Les lieux",
|
"title": "Les lieux",
|
||||||
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
|
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
|
||||||
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
|
"update_success": "Lieu mis à jour !",
|
||||||
|
"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils",
|
||||||
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
|
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
|
||||||
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",
|
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",
|
||||||
|
"upgrade_options_available": "Voici toutes les révisions disponibles, veuillez sélectionner celle vers laquelle vous souhaitez que TOUS les appareils de ce lieu soient mis à niveau",
|
||||||
"use_existing": "Utiliser l'existant",
|
"use_existing": "Utiliser l'existant",
|
||||||
"use_existing_contacts": "Utiliser les contacts existants",
|
"use_existing_contacts": "Utiliser les contacts existants",
|
||||||
"use_this_contact": "Utilisez ce contact"
|
"use_this_contact": "Utilisez ce contact"
|
||||||
|
|||||||
@@ -79,8 +79,11 @@
|
|||||||
"live_view_help": "Ajuda da visualização ao vivo",
|
"live_view_help": "Ajuda da visualização ao vivo",
|
||||||
"memory": "Memória",
|
"memory": "Memória",
|
||||||
"memory_used": "Memória Usada",
|
"memory_used": "Memória Usada",
|
||||||
"missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior",
|
"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
|
"monitoring": "Monitoramento",
|
||||||
|
"no_board": "Sem monitoramento",
|
||||||
|
"no_board_description": "Você não está monitorando este local no momento, clique aqui para começar",
|
||||||
"noise": "Barulho",
|
"noise": "Barulho",
|
||||||
"packets": "Pacotes",
|
"packets": "Pacotes",
|
||||||
"radio": "Rádio",
|
"radio": "Rádio",
|
||||||
@@ -91,6 +94,8 @@
|
|||||||
"retries": "Novas tentativas",
|
"retries": "Novas tentativas",
|
||||||
"search_serials": "Pesquisar séries",
|
"search_serials": "Pesquisar séries",
|
||||||
"stop_monitoring": "Parar o monitoramento",
|
"stop_monitoring": "Parar o monitoramento",
|
||||||
|
"stop_monitoring_success": "Local de monitoramento interrompido!",
|
||||||
|
"stop_monitoring_warning": "Tem certeza? Isso apagará todos os dados de monitoramento gravados para este local",
|
||||||
"temperature": "Temperatura",
|
"temperature": "Temperatura",
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"total_data": "Dados totais",
|
"total_data": "Dados totais",
|
||||||
@@ -175,6 +180,7 @@
|
|||||||
"other": "comandos",
|
"other": "comandos",
|
||||||
"override_dfs": "Substituir DFS",
|
"override_dfs": "Substituir DFS",
|
||||||
"reboot": "Reiniciar",
|
"reboot": "Reiniciar",
|
||||||
|
"reboot_description": "Deseja reiniciar este dispositivo?",
|
||||||
"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}",
|
"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}",
|
||||||
"reboot_success": "Comando de reinicialização enviado com sucesso!",
|
"reboot_success": "Comando de reinicialização enviado com sucesso!",
|
||||||
"revision": "revisão",
|
"revision": "revisão",
|
||||||
@@ -391,6 +397,7 @@
|
|||||||
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
|
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
|
||||||
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
|
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
|
"wifi_bands_max": "Não pode haver mais de 8 SSIDs usando esta banda wi-fi",
|
||||||
"wifi_frames": "Quadros WiFi"
|
"wifi_frames": "Quadros WiFi"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -600,6 +607,7 @@
|
|||||||
"certificate_expires_in": "Certificado expira em",
|
"certificate_expires_in": "Certificado expira em",
|
||||||
"certificate_expiry": "Certificado expira em",
|
"certificate_expiry": "Certificado expira em",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"crash_logs": "Registros de falhas",
|
||||||
"create_errors": "erros ao tentar criar dispositivos",
|
"create_errors": "erros ao tentar criar dispositivos",
|
||||||
"create_success": " dispositivos criados com sucesso",
|
"create_success": " dispositivos criados com sucesso",
|
||||||
"current_firmware": "Firmware atual",
|
"current_firmware": "Firmware atual",
|
||||||
@@ -613,6 +621,7 @@
|
|||||||
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
|
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
|
||||||
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
|
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
|
||||||
|
"logs_one": "Registro",
|
||||||
"new_devices": "novos dispositivos",
|
"new_devices": "novos dispositivos",
|
||||||
"no_model_image": "Nenhuma imagem de modelo encontrada",
|
"no_model_image": "Nenhuma imagem de modelo encontrada",
|
||||||
"not_connected": "Não conectado",
|
"not_connected": "Não conectado",
|
||||||
@@ -621,6 +630,8 @@
|
|||||||
"one": "Dispositivo",
|
"one": "Dispositivo",
|
||||||
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
|
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
|
||||||
"restricted": "Restrito",
|
"restricted": "Restrito",
|
||||||
|
"restricted_overriden": "Este é um dispositivo restrito, mas está em modo de desenvolvimento. Todas as restrições são atualmente ignoradas",
|
||||||
|
"restrictions_overriden_title": "Modo de desenvolvedor",
|
||||||
"sanity": "Sanidade",
|
"sanity": "Sanidade",
|
||||||
"start_import": "Iniciar importação de dispositivos",
|
"start_import": "Iniciar importação de dispositivos",
|
||||||
"test_batch": "Dados de importação de teste",
|
"test_batch": "Dados de importação de teste",
|
||||||
@@ -671,7 +682,15 @@
|
|||||||
"test_digicert_creds": "Credenciais de teste",
|
"test_digicert_creds": "Credenciais de teste",
|
||||||
"title": "Entidades",
|
"title": "Entidades",
|
||||||
"tree": "Árvore de entidades",
|
"tree": "Árvore de entidades",
|
||||||
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas."
|
"update_success": "Entidade atualizada!",
|
||||||
|
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
|
||||||
|
"last_db_update_modal": "banco de dados de firmware",
|
||||||
|
"last_db_update_title": "base de dados",
|
||||||
|
"start_db_update": "Iniciar atualização do banco de dados",
|
||||||
|
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Distribuído por",
|
"powered_by": "Distribuído por",
|
||||||
@@ -765,13 +784,17 @@
|
|||||||
"city": "Cidade",
|
"city": "Cidade",
|
||||||
"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo",
|
"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo",
|
||||||
"country": "País",
|
"country": "País",
|
||||||
|
"elevation": "elevação",
|
||||||
"geocode": "Código geográfico",
|
"geocode": "Código geográfico",
|
||||||
|
"lat": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
"one": "Localização",
|
"one": "Localização",
|
||||||
"other": "Localizações",
|
"other": "Localizações",
|
||||||
"postal": "CEP / Código Postal",
|
"postal": "CEP / Código Postal",
|
||||||
"state": "Estado / Província",
|
"state": "Estado / Província",
|
||||||
"title": "Localizações",
|
"title": "Localizações",
|
||||||
"to_claim": "Locais para reivindicar"
|
"to_claim": "Locais para reivindicar",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Política de Acesso",
|
"access_policy": "Política de Acesso",
|
||||||
@@ -797,6 +820,7 @@
|
|||||||
"reset_password": "Redefinir senha",
|
"reset_password": "Redefinir senha",
|
||||||
"sign_in": "assinar em",
|
"sign_in": "assinar em",
|
||||||
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
|
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
|
||||||
|
"suspended_error": "Conta suspensa, entre em contato com seu administrador",
|
||||||
"verification": "Verifique seu login",
|
"verification": "Verifique seu login",
|
||||||
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
|
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
|
||||||
"welcome_back": "Bem vindo de volta!",
|
"welcome_back": "Bem vindo de volta!",
|
||||||
@@ -902,7 +926,7 @@
|
|||||||
"dfs": "Substituição DFS",
|
"dfs": "Substituição DFS",
|
||||||
"gw_commands": "Comandos de gateway",
|
"gw_commands": "Comandos de gateway",
|
||||||
"identifier": "Identificador",
|
"identifier": "Identificador",
|
||||||
"key_verification": "Verificação da chave de assinatura",
|
"key_verification": "Informações Chave de Assinatura",
|
||||||
"restricted": "Restrito",
|
"restricted": "Restrito",
|
||||||
"signed_upgrade": "Somente atualização assinada",
|
"signed_upgrade": "Somente atualização assinada",
|
||||||
"title": "RESTRIÇÕES",
|
"title": "RESTRIÇÕES",
|
||||||
@@ -1022,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Registros de back-end",
|
"backend_logs": "Registros de back-end",
|
||||||
|
"configuration": "Configuração",
|
||||||
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
|
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
|
||||||
"endpoint": "Ponto final",
|
"endpoint": "Ponto final",
|
||||||
"hostname": "Nome de anfitrião",
|
"hostname": "Nome de anfitrião",
|
||||||
@@ -1032,6 +1057,10 @@
|
|||||||
"os": "Sistema Operacional",
|
"os": "Sistema Operacional",
|
||||||
"processors": "Processadores",
|
"processors": "Processadores",
|
||||||
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
|
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
|
||||||
|
"secrets": "Segredos",
|
||||||
|
"secrets_create": "Criar Segredo",
|
||||||
|
"secrets_one": "Segredo",
|
||||||
|
"services": "Serviços",
|
||||||
"start": "Começar",
|
"start": "Começar",
|
||||||
"subsystems": "Subsistemas",
|
"subsystems": "Subsistemas",
|
||||||
"success_reload": "Comando de recarga enviado com sucesso!",
|
"success_reload": "Comando de recarga enviado com sucesso!",
|
||||||
@@ -1053,9 +1082,11 @@
|
|||||||
"previous_page": "Página anterior"
|
"previous_page": "Página anterior"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "e-mail não validado",
|
||||||
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
|
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
|
||||||
"password": "Senha",
|
"password": "Senha",
|
||||||
"role": "Função",
|
"role": "Função",
|
||||||
|
"suspended": "Suspenso",
|
||||||
"title": "Do utilizador"
|
"title": "Do utilizador"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1131,11 @@
|
|||||||
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
|
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
|
||||||
"title": "Locais",
|
"title": "Locais",
|
||||||
"update_all_devices": "Atualizar todas as configurações do dispositivo",
|
"update_all_devices": "Atualizar todas as configurações do dispositivo",
|
||||||
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
|
"update_success": "Local atualizado!",
|
||||||
|
"upgrade_all_devices": "Atualize o firmware de todos os dispositivos",
|
||||||
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
|
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
|
||||||
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
|
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
|
||||||
|
"upgrade_options_available": "Aqui estão todas as revisões disponíveis, selecione aquela para a qual você deseja que TODOS os dispositivos deste local sejam atualizados",
|
||||||
"use_existing": "Usar existente",
|
"use_existing": "Usar existente",
|
||||||
"use_existing_contacts": "Usar contatos existentes",
|
"use_existing_contacts": "Usar contatos existentes",
|
||||||
"use_this_contact": "Use este contato"
|
"use_this_contact": "Use este contato"
|
||||||
|
|||||||
@@ -12,7 +12,14 @@ export interface AlertButtonProps extends ThemeProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _AlertButton: React.FC<AlertButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
const _AlertButton: React.FC<AlertButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
isCompact = true,
|
||||||
|
label,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ export interface CreateButtonProps extends SpaceProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _CreateButton: React.FC<CreateButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
const _CreateButton: React.FC<CreateButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
isCompact = true,
|
||||||
|
label,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const _DeleteButton: React.FC<DeleteButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
label,
|
label,
|
||||||
ml,
|
ml,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react';
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Portal,
|
||||||
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Wrench } from 'phosphor-react';
|
import { Wrench } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import RebootMenuItem from './RebootButton';
|
|
||||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
|
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
|
||||||
import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware';
|
import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware';
|
||||||
@@ -22,6 +32,7 @@ interface Props {
|
|||||||
onOpenConfigureModal: (serialNumber: string) => void;
|
onOpenConfigureModal: (serialNumber: string) => void;
|
||||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||||
|
onOpenRebootModal: (serialNumber: string) => void;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
isCompact?: boolean;
|
isCompact?: boolean;
|
||||||
}
|
}
|
||||||
@@ -38,8 +49,9 @@ const DeviceActionDropdown = ({
|
|||||||
onOpenTelemetryModal,
|
onOpenTelemetryModal,
|
||||||
onOpenConfigureModal,
|
onOpenConfigureModal,
|
||||||
onOpenScriptModal,
|
onOpenScriptModal,
|
||||||
|
onOpenRebootModal,
|
||||||
size,
|
size,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -145,11 +157,13 @@ const DeviceActionDropdown = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConnectClick = () => getRtty();
|
const handleConnectClick = () => getRtty();
|
||||||
|
const handleRebootClick = () => onOpenRebootModal(device.serialNumber);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Tooltip label={t('commands.other')}>
|
<Tooltip label={t('common.actions')}>
|
||||||
{size === undefined || isCompact ? (
|
{size === undefined || isCompact ? (
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
@@ -168,26 +182,28 @@ const DeviceActionDropdown = ({
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
ml={2}
|
ml={2}
|
||||||
>
|
>
|
||||||
{t('commands.other')}
|
{t('common.actions')}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
)}
|
)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<MenuList>
|
<Portal>
|
||||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
<MenuList>
|
||||||
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
|
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||||
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
|
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
|
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
||||||
<RebootMenuItem device={device} refresh={refresh} />
|
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
<MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
||||||
<MenuItem onClick={handleUpdateToLatest} hidden>
|
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
||||||
{t('premium.toolbox.upgrade_to_latest')}
|
<MenuItem onClick={handleUpdateToLatest} hidden>
|
||||||
</MenuItem>
|
{t('premium.toolbox.upgrade_to_latest')}
|
||||||
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
|
</MenuItem>
|
||||||
</MenuList>
|
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Portal>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { Pen } from 'phosphor-react';
|
import { Pen } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface EditButtonProps {
|
export interface EditButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@@ -11,7 +12,15 @@ export interface EditButtonProps {
|
|||||||
ml?: string | number;
|
ml?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, isLoading, isCompact, ...props }) => {
|
const _EditButton: React.FC<EditButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
label,
|
||||||
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
isCompact = true,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
|
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
|
||||||
@@ -24,12 +33,12 @@ const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, is
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{label}
|
{label ?? t('common.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label}>
|
<Tooltip label={label ?? t('common.edit')} hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="edit"
|
aria-label="edit"
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const _RefreshButton: React.FC<RefreshButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isFetching,
|
isFetching,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
ml,
|
ml,
|
||||||
size,
|
size,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const _ResponsiveButton: React.FC<ResponsiveButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
color,
|
color,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const _SaveButton: React.FC<SaveButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
isDirty,
|
isDirty,
|
||||||
dirtyCheck,
|
dirtyCheck,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
|
|||||||
isDirty,
|
isDirty,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
ml,
|
ml,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const _WarningButton: React.FC<WarningButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
label,
|
label,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const ColumnPicker = ({
|
|||||||
hiddenColumns,
|
hiddenColumns,
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
size,
|
size,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
}: ColumnPickerProps) => {
|
}: ColumnPickerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getPref, setPref } = useAuth();
|
const { getPref, setPref } = useAuth();
|
||||||
|
|||||||
@@ -44,15 +44,18 @@ const defaultProps = {
|
|||||||
sortBy: [],
|
sortBy: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableProps = {
|
export type DataTableProps<TValue> = {
|
||||||
columns: readonly Column<object>[];
|
columns: Column<TValue>[];
|
||||||
data: object[];
|
data: TValue[];
|
||||||
count?: number;
|
count?: number;
|
||||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
onRowClick?: (row: TValue) => void;
|
||||||
|
isRowClickable?: (row: TValue) => boolean;
|
||||||
obj?: string;
|
obj?: string;
|
||||||
sortBy?: { id: string; desc: boolean }[];
|
sortBy?: { id: string; desc: boolean }[];
|
||||||
hiddenColumns?: string[];
|
hiddenColumns?: string[];
|
||||||
|
hideEmptyListText?: boolean;
|
||||||
hideControls?: boolean;
|
hideControls?: boolean;
|
||||||
minHeight?: string | number;
|
minHeight?: string | number;
|
||||||
fullScreen?: boolean;
|
fullScreen?: boolean;
|
||||||
@@ -67,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
|||||||
state: UsePaginationState<T>;
|
state: UsePaginationState<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _DataTable = ({
|
const _DataTable = <TValue extends object>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -77,15 +80,19 @@ const _DataTable = ({
|
|||||||
sortBy,
|
sortBy,
|
||||||
hiddenColumns,
|
hiddenColumns,
|
||||||
hideControls,
|
hideControls,
|
||||||
|
hideEmptyListText,
|
||||||
count,
|
count,
|
||||||
setPageInfo,
|
setPageInfo,
|
||||||
isManual,
|
isManual,
|
||||||
saveSettingsId,
|
saveSettingsId,
|
||||||
showAllRows,
|
showAllRows,
|
||||||
}: DataTableProps) => {
|
onRowClick,
|
||||||
|
isRowClickable,
|
||||||
|
}: DataTableProps<TValue>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
const textColor = useColorModeValue('gray.700', 'white');
|
||||||
|
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||||
const getPageSize = () => {
|
const getPageSize = () => {
|
||||||
try {
|
try {
|
||||||
if (showAllRows) return 1000000;
|
if (showAllRows) return 1000000;
|
||||||
@@ -140,8 +147,12 @@ const _DataTable = ({
|
|||||||
},
|
},
|
||||||
useSortBy,
|
useSortBy,
|
||||||
usePagination,
|
usePagination,
|
||||||
) as TableInstanceWithHooks<object>;
|
) as TableInstanceWithHooks<TValue>;
|
||||||
|
|
||||||
|
const handleGoToPage = (newPage: number) => {
|
||||||
|
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
|
||||||
|
gotoPage(newPage);
|
||||||
|
};
|
||||||
const handleNextPage = () => {
|
const handleNextPage = () => {
|
||||||
nextPage();
|
nextPage();
|
||||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1));
|
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1));
|
||||||
@@ -253,10 +264,19 @@ const _DataTable = ({
|
|||||||
</Thead>
|
</Thead>
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
<Tbody {...getTableBodyProps()}>
|
<Tbody {...getTableBodyProps()}>
|
||||||
{page.map((row: Row) => {
|
{page.map((row: Row<TValue>) => {
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
|
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
|
||||||
|
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
|
||||||
return (
|
return (
|
||||||
<Tr {...row.getRowProps()} key={uuid()}>
|
<Tr
|
||||||
|
{...row.getRowProps()}
|
||||||
|
key={uuid()}
|
||||||
|
_hover={{
|
||||||
|
backgroundColor: hoveredRowBg,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
row.cells.map((cell) => (
|
row.cells.map((cell) => (
|
||||||
@@ -275,8 +295,26 @@ const _DataTable = ({
|
|||||||
fontSize="14px"
|
fontSize="14px"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||||
// @ts-ignore
|
fontFamily={
|
||||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
// @ts-ignore
|
||||||
|
cell.column.isMonospace
|
||||||
|
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
// @ts-ignore
|
||||||
|
cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick)
|
||||||
|
? (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
cursor={
|
||||||
|
// @ts-ignore
|
||||||
|
!cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick
|
||||||
|
? 'pointer'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render('Cell')}
|
||||||
</Td>
|
</Td>
|
||||||
@@ -288,7 +326,7 @@ const _DataTable = ({
|
|||||||
</Tbody>
|
</Tbody>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
{!isLoading && data.length === 0 && (
|
{!isLoading && data.length === 0 && !hideEmptyListText && (
|
||||||
<Center>
|
<Center>
|
||||||
{obj ? (
|
{obj ? (
|
||||||
<Heading size="md" pt={12}>
|
<Heading size="md" pt={12}>
|
||||||
@@ -309,7 +347,7 @@ const _DataTable = ({
|
|||||||
<Tooltip label={t('table.first_page')}>
|
<Tooltip label={t('table.first_page')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Go to first page"
|
aria-label="Go to first page"
|
||||||
onClick={() => gotoPage(0)}
|
onClick={() => handleGoToPage(0)}
|
||||||
isDisabled={!canPreviousPage}
|
isDisabled={!canPreviousPage}
|
||||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||||
mr={4}
|
mr={4}
|
||||||
@@ -347,7 +385,7 @@ const _DataTable = ({
|
|||||||
max={pageOptions.length}
|
max={pageOptions.length}
|
||||||
onChange={(_: unknown, numberValue: number) => {
|
onChange={(_: unknown, numberValue: number) => {
|
||||||
const newPage = numberValue ? numberValue - 1 : 0;
|
const newPage = numberValue ? numberValue - 1 : 0;
|
||||||
gotoPage(newPage);
|
handleGoToPage(newPage);
|
||||||
}}
|
}}
|
||||||
defaultValue={pageIndex + 1}
|
defaultValue={pageIndex + 1}
|
||||||
>
|
>
|
||||||
@@ -386,7 +424,7 @@ const _DataTable = ({
|
|||||||
<Tooltip label={t('table.last_page')}>
|
<Tooltip label={t('table.last_page')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Go to last page"
|
aria-label="Go to last page"
|
||||||
onClick={() => gotoPage(pageCount - 1)}
|
onClick={() => handleGoToPage(pageCount - 1)}
|
||||||
isDisabled={!canNextPage}
|
isDisabled={!canNextPage}
|
||||||
icon={<ArrowRightIcon h={3} w={3} />}
|
icon={<ArrowRightIcon h={3} w={3} />}
|
||||||
ml={4}
|
ml={4}
|
||||||
@@ -401,4 +439,4 @@ const _DataTable = ({
|
|||||||
|
|
||||||
_DataTable.defaultProps = defaultProps;
|
_DataTable.defaultProps = defaultProps;
|
||||||
|
|
||||||
export const DataTable = React.memo(_DataTable);
|
export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable;
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const SortableDataTable: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
const textColor = useColorModeValue('gray.700', 'white');
|
||||||
const getPageSize = () => {
|
const getPageSize = () => {
|
||||||
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
|
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
|
||||||
@@ -223,7 +224,13 @@ const SortableDataTable: React.FC<Props> = ({
|
|||||||
{page.map((row: Row) => {
|
{page.map((row: Row) => {
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
return (
|
return (
|
||||||
<Tr {...row.getRowProps()} key={uuid()}>
|
<Tr
|
||||||
|
{...row.getRowProps()}
|
||||||
|
key={uuid()}
|
||||||
|
_hover={{
|
||||||
|
backgroundColor: hoveredRowBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
row.cells.map((cell) => (
|
row.cells.map((cell) => (
|
||||||
@@ -242,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({
|
|||||||
fontSize="14px"
|
fontSize="14px"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||||
// @ts-ignore
|
fontFamily={
|
||||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
// @ts-ignore
|
||||||
|
cell.column.isMonospace
|
||||||
|
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render('Cell')}
|
||||||
</Td>
|
</Td>
|
||||||
|
|||||||
27
src/components/Maps/GoogleMap/Marker.tsx
Normal file
27
src/components/Maps/GoogleMap/Marker.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const _GoogleMapMarker = (options: google.maps.MarkerOptions) => {
|
||||||
|
const [marker, setMarker] = React.useState<google.maps.Marker>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!marker) {
|
||||||
|
setMarker(new google.maps.Marker());
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (marker) {
|
||||||
|
marker.setMap(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [marker]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (marker) {
|
||||||
|
marker.setOptions(options);
|
||||||
|
}
|
||||||
|
}, [marker, options]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GoogleMapMarker = React.memo(_GoogleMapMarker);
|
||||||
89
src/components/Maps/GoogleMap/index.tsx
Normal file
89
src/components/Maps/GoogleMap/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { isLatLngLiteral } from '@googlemaps/typescript-guards';
|
||||||
|
import { createCustomEqual } from 'fast-equals';
|
||||||
|
|
||||||
|
const deepCompareEqualsForMaps = createCustomEqual((deepEqual) =>
|
||||||
|
// @ts-ignore
|
||||||
|
(a: number | google.maps.LatLng | google.maps.LatLngLiteral, b: number | google.maps.LatLng | google.maps.LatLngLiteral) => {
|
||||||
|
if (
|
||||||
|
isLatLngLiteral(a) ||
|
||||||
|
a instanceof google.maps.LatLng ||
|
||||||
|
isLatLngLiteral(b) ||
|
||||||
|
b instanceof google.maps.LatLng
|
||||||
|
) {
|
||||||
|
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return deepEqual(a, b);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const useDeepCompareMemoize = (value: unknown) => {
|
||||||
|
const ref = React.useRef<unknown>();
|
||||||
|
|
||||||
|
if (!deepCompareEqualsForMaps(value, ref.current)) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDeepCompareEffectForMaps = (callback: React.EffectCallback, dependencies: unknown[]) => {
|
||||||
|
React.useEffect(callback, dependencies.map(useDeepCompareMemoize));
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GoogleMapProps extends google.maps.MapOptions {
|
||||||
|
style: { [key: string]: string };
|
||||||
|
onClick?: (e: google.maps.MapMouseEvent) => void;
|
||||||
|
onIdle?: (map: google.maps.Map) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _GoogleMap = ({ style, onClick, onIdle, children, ...options }: GoogleMapProps) => {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [map, setMap] = React.useState<google.maps.Map>();
|
||||||
|
|
||||||
|
// because React does not do deep comparisons, a custom hook is used
|
||||||
|
useDeepCompareEffectForMaps(() => {
|
||||||
|
if (map) {
|
||||||
|
map.setOptions(options);
|
||||||
|
}
|
||||||
|
}, [map, options]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (ref.current && !map) {
|
||||||
|
setMap(new window.google.maps.Map(ref.current, {}));
|
||||||
|
}
|
||||||
|
}, [ref, map]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (map) {
|
||||||
|
['click', 'idle'].forEach((eventName) => google.maps.event.clearListeners(map, eventName));
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
map.addListener('click', onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onIdle) {
|
||||||
|
map.addListener('idle', () => onIdle(map));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [map, onClick, onIdle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={ref} style={style} />
|
||||||
|
{React.Children.map(children, (child) => {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
// set the map prop on the child component
|
||||||
|
// @ts-ignore
|
||||||
|
return React.cloneElement(child, { map });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GoogleMap = React.memo(_GoogleMap);
|
||||||
@@ -5,17 +5,22 @@ import {
|
|||||||
AlertIcon,
|
AlertIcon,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Textarea,
|
Textarea,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { ClipboardText } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SaveButton } from '../../Buttons/SaveButton';
|
import { SaveButton } from '../../Buttons/SaveButton';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
import { FileInputButton } from 'components/Buttons/FileInputButton';
|
import { FileInputButton } from 'components/Buttons/FileInputButton';
|
||||||
import { useConfigureDevice } from 'hooks/Network/Commands';
|
import { useConfigureDevice } from 'hooks/Network/Commands';
|
||||||
|
import { useGetDevice } from 'hooks/Network/Devices';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
export type ConfigureModalProps = {
|
export type ConfigureModalProps = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
@@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const configure = useConfigureDevice({ serialNumber });
|
const configure = useConfigureDevice({ serialNumber });
|
||||||
|
const getDevice = useGetDevice({ serialNumber });
|
||||||
|
|
||||||
const [newConfig, setNewConfig] = React.useState('');
|
const [newConfig, setNewConfig] = React.useState('');
|
||||||
|
|
||||||
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setNewConfig(e.target.value);
|
setNewConfig(e.target.value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onImportConfiguration = () => {
|
||||||
|
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
|
||||||
|
};
|
||||||
const isValid = React.useMemo(() => {
|
const isValid = React.useMemo(() => {
|
||||||
try {
|
try {
|
||||||
JSON.parse(newConfig);
|
JSON.parse(newConfig);
|
||||||
@@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
modalProps.onClose();
|
modalProps.onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// console.log(e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Box>
|
<Box>
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
{
|
<AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription>
|
||||||
// @ts-ignore
|
|
||||||
<AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription>
|
|
||||||
}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -92,15 +98,25 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
</Alert>
|
</Alert>
|
||||||
<FormControl isInvalid={!isValid && newConfig.length > 0}>
|
<FormControl isInvalid={!isValid && newConfig.length > 0}>
|
||||||
<FormLabel>{t('configurations.one')}</FormLabel>
|
<FormLabel>{t('configurations.one')}</FormLabel>
|
||||||
<Box mb={2} w="240px">
|
<Flex mb={2}>
|
||||||
<FileInputButton
|
<Box w="240px">
|
||||||
value={newConfig}
|
<FileInputButton
|
||||||
setValue={(v) => setNewConfig(v)}
|
value={newConfig}
|
||||||
refreshId="1"
|
setValue={(v) => setNewConfig(v)}
|
||||||
accept=".json"
|
refreshId="1"
|
||||||
isStringFile
|
accept=".json"
|
||||||
/>
|
isStringFile
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
rightIcon={<ClipboardText size={20} />}
|
||||||
|
onClick={onImportConfiguration}
|
||||||
|
hidden={!getDevice.data}
|
||||||
|
ml={2}
|
||||||
|
>
|
||||||
|
Current Configuration
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
<Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} />
|
<Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} />
|
||||||
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
|
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
|||||||
upgrade({
|
upgrade({
|
||||||
keepRedirector: isRedirector,
|
keepRedirector: isRedirector,
|
||||||
uri,
|
uri,
|
||||||
signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined,
|
signature:
|
||||||
|
device?.restrictedDevice && !device?.restrictionDetails?.developer ? ref.current?.values?.signature : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +90,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" />
|
<Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{device?.restrictedDevice && (
|
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
|
||||||
<Formik<{ signature?: string }>
|
<Formik<{ signature?: string }>
|
||||||
innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined}
|
innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined}
|
||||||
key={formKey}
|
key={formKey}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
||||||
|
|
||||||
export interface ModalHeaderProps {
|
export interface ModalHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
left?: React.ReactNode;
|
||||||
right: React.ReactNode;
|
right: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, right }) => (
|
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => (
|
||||||
<Header>
|
<Header>
|
||||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||||
{title}
|
{title}
|
||||||
|
<HStack spacing={2} ml={2}>
|
||||||
|
{left ?? null}
|
||||||
|
</HStack>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{right}
|
{right}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type ModalProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
topRightButtons?: React.ReactNode;
|
topRightButtons?: React.ReactNode;
|
||||||
|
tags?: React.ReactNode;
|
||||||
options?: {
|
options?: {
|
||||||
modalSize?: 'sm' | 'md' | 'lg';
|
modalSize?: 'sm' | 'md' | 'lg';
|
||||||
maxWidth?: LayoutProps['maxWidth'];
|
maxWidth?: LayoutProps['maxWidth'];
|
||||||
@@ -15,7 +16,7 @@ export type ModalProps = {
|
|||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => {
|
const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => {
|
||||||
const maxWidth = React.useMemo(() => {
|
const maxWidth = React.useMemo(() => {
|
||||||
if (options?.maxWidth) return options.maxWidth;
|
if (options?.maxWidth) return options.maxWidth;
|
||||||
if (options?.modalSize === 'sm') return undefined;
|
if (options?.modalSize === 'sm') return undefined;
|
||||||
@@ -32,6 +33,7 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }:
|
|||||||
<ModalContent maxWidth={maxWidth}>
|
<ModalContent maxWidth={maxWidth}>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
title={title}
|
title={title}
|
||||||
|
left={tags}
|
||||||
right={
|
right={
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
{topRightButtons}
|
{topRightButtons}
|
||||||
|
|||||||
@@ -1,40 +1,42 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { MenuItem, useToast } from '@chakra-ui/react';
|
import { Alert, AlertIcon, Box, Button, Center, useToast } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Modal } from '../Modal';
|
||||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
import { useRebootDevice } from 'hooks/Network/Devices';
|
import { useRebootDevice } from 'hooks/Network/Devices';
|
||||||
import { useMutationResult } from 'hooks/useMutationResult';
|
import { useMutationResult } from 'hooks/useMutationResult';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { GatewayDevice } from 'models/Device';
|
|
||||||
|
|
||||||
type Props = {
|
export type RebootModalProps = {
|
||||||
device: GatewayDevice;
|
serialNumber: string;
|
||||||
refresh: () => void;
|
modalProps: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const RebootMenuItem = ({ device, refresh }: Props) => {
|
export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
||||||
const { mutateAsync: reboot } = useRebootDevice({ serialNumber: device.serialNumber });
|
const { mutateAsync: reboot, isLoading } = useRebootDevice({ serialNumber });
|
||||||
const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({
|
const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({
|
||||||
objName: t('devices.one'),
|
objName: t('devices.one'),
|
||||||
operationType: 'reboot',
|
operationType: 'reboot',
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
refresh();
|
|
||||||
addEventListeners([
|
addEventListeners([
|
||||||
{
|
{
|
||||||
id: `device-connection-${device.serialNumber}`,
|
id: `device-connection-${serialNumber}`,
|
||||||
type: 'DEVICE_CONNECTION',
|
type: 'DEVICE_CONNECTION',
|
||||||
serialNumber: device.serialNumber,
|
serialNumber,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const id = `device-connection-notification-${device.serialNumber}`;
|
const id = `device-connection-notification-${serialNumber}`;
|
||||||
|
|
||||||
if (!toast.isActive(id)) {
|
if (!toast.isActive(id)) {
|
||||||
toast({
|
toast({
|
||||||
id,
|
id,
|
||||||
title: t('common.success'),
|
title: t('common.success'),
|
||||||
description: t('controller.devices.finished_reboot', { serialNumber: device.serialNumber }),
|
description: t('controller.devices.finished_reboot', { serialNumber }),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@@ -44,17 +46,17 @@ const RebootMenuItem = ({ device, refresh }: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `device-disconnected-${device.serialNumber}`,
|
id: `device-disconnected-${serialNumber}`,
|
||||||
type: 'DEVICE_DISCONNECTION',
|
type: 'DEVICE_DISCONNECTION',
|
||||||
serialNumber: device.serialNumber,
|
serialNumber,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const id = `device-disconnection-notification-${device.serialNumber}`;
|
const id = `device-disconnection-notification-${serialNumber}`;
|
||||||
|
|
||||||
if (!toast.isActive(id)) {
|
if (!toast.isActive(id)) {
|
||||||
toast({
|
toast({
|
||||||
id,
|
id,
|
||||||
title: t('common.success'),
|
title: t('common.success'),
|
||||||
description: t('controller.devices.started_reboot', { serialNumber: device.serialNumber }),
|
description: t('controller.devices.started_reboot', { serialNumber }),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@@ -66,17 +68,39 @@ const RebootMenuItem = ({ device, refresh }: Props) => {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRebootClick = () =>
|
const handleRebootClick = () =>
|
||||||
reboot(undefined, {
|
reboot(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
onRebootSuccess();
|
onRebootSuccess();
|
||||||
|
modalProps.onClose();
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
onRebootError(e as AxiosError);
|
onRebootError(e as AxiosError);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>;
|
return (
|
||||||
|
<Modal
|
||||||
|
{...modalProps}
|
||||||
|
title={t('commands.reboot')}
|
||||||
|
topRightButtons={
|
||||||
|
<Button colorScheme="blue" onClick={handleRebootClick} isLoading={isLoading}>
|
||||||
|
{t('commands.reboot')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
options={{
|
||||||
|
modalSize: 'sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Center mb={2}>
|
||||||
|
<Alert status="info" w="unset">
|
||||||
|
<AlertIcon />
|
||||||
|
{t('commands.reboot_description')}
|
||||||
|
</Alert>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RebootMenuItem;
|
|
||||||
@@ -183,7 +183,9 @@ const CustomScriptForm = ({
|
|||||||
<>
|
<>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Box>
|
<Box>
|
||||||
{device?.restrictedDevice && <SignatureField name="signature" isDisabled={areFieldsDisabled} />}
|
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
|
||||||
|
<SignatureField name="signature" isDisabled={areFieldsDisabled} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
|||||||
let requestData: {
|
let requestData: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
|
script?: string;
|
||||||
timeout?: number | undefined;
|
timeout?: number | undefined;
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
if (requestData.script) {
|
||||||
|
requestData.script = btoa(requestData.script);
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedScript === 'diagnostics') {
|
if (selectedScript === 'diagnostics') {
|
||||||
requestData = {
|
requestData = {
|
||||||
serialNumber: device?.serialNumber ?? '',
|
serialNumber: device?.serialNumber ?? '',
|
||||||
@@ -76,7 +82,7 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
|||||||
when: 0,
|
when: 0,
|
||||||
deferred: data.deferred,
|
deferred: data.deferred,
|
||||||
timeout: data.timeout,
|
timeout: data.timeout,
|
||||||
signature: device?.restrictedDevice ? data.signature : undefined,
|
signature: device?.restrictedDevice && !device?.restrictionDetails?.developer ? data.signature : undefined,
|
||||||
uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined,
|
uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined,
|
||||||
scriptId: selectedScript,
|
scriptId: selectedScript,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
@@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
|||||||
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
|
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
|
||||||
queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']);
|
queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']);
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e) && e.response?.data?.ErrorDescription) {
|
||||||
|
toast({
|
||||||
|
id: 'script-update-error',
|
||||||
|
title: t('common.error'),
|
||||||
|
description: e.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!waitForResponse) {
|
if (!waitForResponse) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
if (isOpen) resetData();
|
if (isOpen) resetData();
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
return (
|
return (
|
||||||
(<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
@@ -66,7 +66,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
<>
|
<>
|
||||||
{csvData ? (
|
{csvData ? (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(<CSVLink
|
<CSVLink
|
||||||
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
data={csvData as object[]}
|
data={csvData as object[]}
|
||||||
>
|
>
|
||||||
@@ -77,7 +77,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
label={t('common.download')}
|
label={t('common.download')}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
/>
|
/>
|
||||||
</CSVLink>)
|
</CSVLink>
|
||||||
) : (
|
) : (
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
color="gray"
|
color="gray"
|
||||||
@@ -118,6 +118,6 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
confirm={closeCancelAndForm}
|
confirm={closeCancelAndForm}
|
||||||
cancel={closeConfirm}
|
cancel={closeConfirm}
|
||||||
/>
|
/>
|
||||||
</Modal>)
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
connectedDevices: msg.statistics.numberOfDevices,
|
connectedDevices: msg.statistics.numberOfDevices,
|
||||||
connectingDevices: msg.statistics.numberOfConnectingDevices,
|
connectingDevices: msg.statistics.numberOfConnectingDevices,
|
||||||
averageConnectionTime: msg.statistics.averageConnectedTime,
|
averageConnectionTime: msg.statistics.averageConnectedTime,
|
||||||
|
tx: msg.statistics.tx,
|
||||||
|
rx: msg.statistics.rx,
|
||||||
},
|
},
|
||||||
queryClient,
|
queryClient,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ type ConnectionStatisticsMessage = {
|
|||||||
numberOfDevices: number;
|
numberOfDevices: number;
|
||||||
numberOfConnectingDevices: number;
|
numberOfConnectingDevices: number;
|
||||||
averageConnectedTime: number;
|
averageConnectedTime: number;
|
||||||
|
tx: number;
|
||||||
|
rx: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
serialNumbers?: undefined;
|
serialNumbers?: undefined;
|
||||||
@@ -85,6 +87,8 @@ export type SocketWebSocketNotificationData =
|
|||||||
numberOfDevices: number;
|
numberOfDevices: number;
|
||||||
numberOfConnectingDevices: number;
|
numberOfConnectingDevices: number;
|
||||||
averageConnectedTime: number;
|
averageConnectedTime: number;
|
||||||
|
rx: number;
|
||||||
|
tx: number;
|
||||||
};
|
};
|
||||||
serialNumber?: undefined;
|
serialNumber?: undefined;
|
||||||
log?: undefined;
|
log?: undefined;
|
||||||
|
|||||||
@@ -110,6 +110,18 @@ export const useDeleteCommand = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSingleCommandHistory = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||||
|
useQuery(
|
||||||
|
['commands', serialNumber, commandId],
|
||||||
|
() =>
|
||||||
|
axiosGw
|
||||||
|
.get(`command/${commandId}?serialNumber=${serialNumber}`)
|
||||||
|
.then((response) => response.data as DeviceCommandHistory),
|
||||||
|
{
|
||||||
|
enabled: serialNumber !== undefined && serialNumber !== '' && commandId !== undefined && commandId !== '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export type EventQueueResponse = {
|
export type EventQueueResponse = {
|
||||||
UUID: string;
|
UUID: string;
|
||||||
attachFile: number;
|
attachFile: number;
|
||||||
@@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
|||||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
toast({
|
toast({
|
||||||
id: 'script-error',
|
id: 'script-error',
|
||||||
@@ -263,14 +276,44 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
|||||||
const downloadScript = (serialNumber: string, commandId: string) =>
|
const downloadScript = (serialNumber: string, commandId: string) =>
|
||||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||||
|
|
||||||
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||||
useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = window.URL.createObjectURL(blob);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
link.download = `Script_${commandId}.tar.gz`;
|
const headerLine =
|
||||||
|
(response.headers['content-disposition'] as string | undefined) ??
|
||||||
|
(response.headers['content-disposition'] as string | undefined);
|
||||||
|
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Script_${commandId}.tar.gz`;
|
||||||
|
link.download = filename;
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
const bufferResponse = e.response?.data;
|
||||||
|
let errorMessage = '';
|
||||||
|
// If the response is a buffer, parse to JSON object
|
||||||
|
if (bufferResponse instanceof ArrayBuffer) {
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||||
|
errorMessage = json.ErrorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: `script-download-error-${serialNumber}`,
|
||||||
|
title: t('common.error'),
|
||||||
|
description: errorMessage,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ export type DeviceLog = {
|
|||||||
severity: number;
|
severity: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceLogs = (limit: number, serialNumber?: string) => async () =>
|
const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1) => async () =>
|
||||||
axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{
|
axiosGw
|
||||||
|
.get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`)
|
||||||
|
.then((response) => response.data) as Promise<{
|
||||||
values: DeviceLog[];
|
values: DeviceLog[];
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -21,20 +23,29 @@ export const useGetDeviceLogs = ({
|
|||||||
serialNumber,
|
serialNumber,
|
||||||
limit,
|
limit,
|
||||||
onError,
|
onError,
|
||||||
|
logType,
|
||||||
}: {
|
}: {
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
|
logType?: 0 | 1;
|
||||||
}) =>
|
}) =>
|
||||||
useQuery(['devicelogs', serialNumber, { limit }], getDeviceLogs(limit, serialNumber), {
|
useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), {
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
staleTime: 30000,
|
staleTime: 30000,
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) =>
|
const deleteLogs = async ({
|
||||||
axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`);
|
serialNumber,
|
||||||
|
endDate,
|
||||||
|
logType,
|
||||||
|
}: {
|
||||||
|
serialNumber: string;
|
||||||
|
endDate: number;
|
||||||
|
logType: 0 | 1;
|
||||||
|
}) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`);
|
||||||
export const useDeleteLogs = () => {
|
export const useDeleteLogs = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -45,46 +56,62 @@ export const useDeleteLogs = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) =>
|
const getLogsBatch = (
|
||||||
|
serialNumber?: string,
|
||||||
|
start?: number,
|
||||||
|
end?: number,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number,
|
||||||
|
logType?: 0 | 1,
|
||||||
|
) =>
|
||||||
axiosGw
|
axiosGw
|
||||||
.get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`)
|
.get(
|
||||||
|
`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`,
|
||||||
|
)
|
||||||
.then((response) => response.data) as Promise<{
|
.then((response) => response.data) as Promise<{
|
||||||
values: DeviceLog[];
|
values: DeviceLog[];
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => {
|
const getDeviceLogsWithTimestamps =
|
||||||
let offset = 0;
|
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1) => async () => {
|
||||||
const limit = 100;
|
let offset = 0;
|
||||||
let logs: DeviceLog[] = [];
|
const limit = 100;
|
||||||
let latestResponse: {
|
let logs: DeviceLog[] = [];
|
||||||
values: DeviceLog[];
|
let latestResponse: {
|
||||||
serialNumber: string;
|
values: DeviceLog[];
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType);
|
||||||
|
logs = logs.concat(latestResponse.values);
|
||||||
|
offset += limit;
|
||||||
|
} while (latestResponse.values.length === limit);
|
||||||
|
return {
|
||||||
|
values: logs,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
do {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset);
|
|
||||||
logs = logs.concat(latestResponse.values);
|
|
||||||
offset += limit;
|
|
||||||
} while (latestResponse.values.length === limit);
|
|
||||||
return {
|
|
||||||
values: logs,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetDeviceLogsWithTimestamps = ({
|
export const useGetDeviceLogsWithTimestamps = ({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
onError,
|
onError,
|
||||||
|
logType,
|
||||||
}: {
|
}: {
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
start?: number;
|
start?: number;
|
||||||
end?: number;
|
end?: number;
|
||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
|
logType?: 0 | 1;
|
||||||
}) =>
|
}) =>
|
||||||
useQuery(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), {
|
useQuery(
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
|
['devicelogs', serialNumber, { start, end, logType }],
|
||||||
staleTime: 1000 * 60,
|
getDeviceLogsWithTimestamps(serialNumber, start, end, logType ?? 0),
|
||||||
onError,
|
{
|
||||||
});
|
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
onError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -165,7 +165,10 @@ export type DevicesStats = {
|
|||||||
averageConnectionTime: number;
|
averageConnectionTime: number;
|
||||||
connectedDevices: number;
|
connectedDevices: number;
|
||||||
connectingDevices: number;
|
connectingDevices: number;
|
||||||
|
tx: number;
|
||||||
|
rx: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialStats = async () =>
|
const getInitialStats = async () =>
|
||||||
axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data);
|
axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data);
|
||||||
export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => {
|
export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => {
|
||||||
|
|||||||
@@ -7,35 +7,49 @@ import { AxiosError } from 'models/Axios';
|
|||||||
import { Firmware } from 'models/Firmware';
|
import { Firmware } from 'models/Firmware';
|
||||||
import { Note } from 'models/Note';
|
import { Note } from 'models/Note';
|
||||||
|
|
||||||
|
const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offset: number) =>
|
||||||
|
axiosFms
|
||||||
|
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
|
||||||
|
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
|
||||||
|
|
||||||
|
const getAllAvailableFirmware = async (deviceType: string) => {
|
||||||
|
const limit = 500;
|
||||||
|
let offset = 0;
|
||||||
|
let data: { firmwares: Firmware[] } = { firmwares: [] };
|
||||||
|
let lastResponse: { firmwares: Firmware[] } = { firmwares: [] };
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
lastResponse = await getAvailableFirmwareBatch(deviceType, limit, offset);
|
||||||
|
data = {
|
||||||
|
firmwares: [...data.firmwares, ...lastResponse.firmwares],
|
||||||
|
};
|
||||||
|
offset += 500;
|
||||||
|
} while (lastResponse.firmwares.length === 500);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => {
|
export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
return useQuery(
|
return useQuery(['get-device-profile'], () => getAllAvailableFirmware(deviceType), {
|
||||||
['get-device-profile'],
|
enabled: deviceType !== '',
|
||||||
() =>
|
onError: (e: AxiosError) => {
|
||||||
axiosFms
|
if (!toast.isActive('firmware-fetching-error'))
|
||||||
.get(`firmwares?deviceType=${deviceType}&limit=10000&offset=0`)
|
toast({
|
||||||
.then(({ data }: { data: { firmwares: Firmware[] } }) => data),
|
id: 'firmware-fetching-error',
|
||||||
{
|
title: t('common.error'),
|
||||||
enabled: deviceType !== '',
|
description: t('crud.error_fetching_obj', {
|
||||||
onError: (e: AxiosError) => {
|
e: e?.response?.data?.ErrorDescription,
|
||||||
if (!toast.isActive('firmware-fetching-error'))
|
obj: t('analytics.firmware'),
|
||||||
toast({
|
}),
|
||||||
id: 'firmware-fetching-error',
|
status: 'error',
|
||||||
title: t('common.error'),
|
duration: 5000,
|
||||||
description: t('crud.error_fetching_obj', {
|
isClosable: true,
|
||||||
e: e?.response?.data?.ErrorDescription,
|
position: 'top-right',
|
||||||
obj: t('analytics.firmware'),
|
});
|
||||||
}),
|
|
||||||
status: 'error',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) =>
|
export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) =>
|
||||||
@@ -56,7 +70,13 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe
|
|||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
|
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
|
||||||
axiosGw.post(`device/${serialNumber}/upgrade`, { serialNumber, when: 0, keepRedirector, uri, signature }),
|
axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
|
||||||
|
serialNumber,
|
||||||
|
when: 0,
|
||||||
|
keepRedirector,
|
||||||
|
uri,
|
||||||
|
signature,
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@@ -222,3 +242,23 @@ export const useGetFirmwareDashboard = () =>
|
|||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getLastDbUpdate = async () =>
|
||||||
|
axiosFms.get(`firmwares?updateTimeOnly=true`).then((response) => response.data as { lastUpdateTime: number });
|
||||||
|
export const useGetFirmwareDbUpdate = () =>
|
||||||
|
useQuery(['firmware', 'db'], getLastDbUpdate, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDb = async () => axiosFms.put(`firmwares?update=true`);
|
||||||
|
|
||||||
|
export const useUpdateFirmwareDb = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(updateDb, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['firmware', 'db']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
@@ -89,3 +89,18 @@ export const useDeleteHealthChecks = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDevicesWithHealthBetween = (
|
||||||
|
context: QueryFunctionContext<[string, string, { lowerLimit: number; upperLimit: number }]>,
|
||||||
|
) =>
|
||||||
|
axiosGw
|
||||||
|
.get(`devices?health=true&lowLimit=${context.queryKey[2].lowerLimit}&highLimit=${context.queryKey[2].upperLimit}`)
|
||||||
|
.then((res) => res.data.serialNumbers as string[]);
|
||||||
|
|
||||||
|
export const useGetDevicesWithHealthBetween = ({
|
||||||
|
lowerLimit,
|
||||||
|
upperLimit,
|
||||||
|
}: {
|
||||||
|
lowerLimit: number;
|
||||||
|
upperLimit: number;
|
||||||
|
}) => useQuery(['devices', 'health', { lowerLimit, upperLimit }], getDevicesWithHealthBetween);
|
||||||
|
|||||||
83
src/hooks/Network/Secrets.ts
Normal file
83
src/hooks/Network/Secrets.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
|
|
||||||
|
export type SecretName = 'google.maps.apikey' | string;
|
||||||
|
|
||||||
|
export type Secret = {
|
||||||
|
key: SecretName;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecretDictionaryValue = {
|
||||||
|
key: SecretName;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecret = async (context: QueryFunctionContext<string[], unknown>) =>
|
||||||
|
axiosSec.get(`/systemSecret/${context.queryKey[1]}`).then(({ data }: { data: Secret }) => data);
|
||||||
|
|
||||||
|
export const useGetSystemSecret = ({ secret }: { secret: SecretName }) =>
|
||||||
|
useQuery(['secrets', secret], getSecret, {
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
refetchInterval: 1000 * 60 * 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAllSecrets = async () =>
|
||||||
|
axiosSec.get('/systemSecret/0?all=true').then(({ data }: { data: { secrets: Secret[] } }) => data.secrets);
|
||||||
|
|
||||||
|
export const useGetAllSystemSecrets = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useQuery(['secrets'], getAllSecrets, {
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
refetchInterval: 1000 * 60 * 10,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
for (const secret of data) {
|
||||||
|
queryClient.setQueryData(['secrets', secret.key], secret);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsDictionary = async () =>
|
||||||
|
axiosSec
|
||||||
|
.get('/systemSecret/0?dictionary=true')
|
||||||
|
.then(({ data }: { data: { knownKeys: SecretDictionaryValue[] } }) => data.knownKeys);
|
||||||
|
|
||||||
|
export const useGetSystemSecretsDictionary = () =>
|
||||||
|
useQuery(['secrets', 'dictionary'], getSecretsDictionary, {
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
refetchInterval: 1000 * 60 * 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateSecret = async ({ key, value }: { key: string; value: string }) =>
|
||||||
|
axiosSec.put(`/systemSecret/${key}?value=${value}`, { key, value });
|
||||||
|
|
||||||
|
export const useUpdateSystemSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(updateSecret, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['secrets']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSystemSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(updateSecret, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['secrets']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSecret = async (key: string) => axiosSec.delete(`/systemSecret/${key}`);
|
||||||
|
|
||||||
|
export const useDeleteSystemSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(deleteSecret, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['secrets']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
@@ -21,6 +21,18 @@ type DeviceInterfaceStatistics = {
|
|||||||
tx_errors: number;
|
tx_errors: number;
|
||||||
tx_packets: number;
|
tx_packets: number;
|
||||||
};
|
};
|
||||||
|
'counters-aggregate'?: {
|
||||||
|
collisions: number;
|
||||||
|
multicast: number;
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_dropped: number;
|
||||||
|
rx_errors: number;
|
||||||
|
rx_packets: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_dropped: number;
|
||||||
|
tx_errors: number;
|
||||||
|
tx_packets: number;
|
||||||
|
};
|
||||||
ssids?: {
|
ssids?: {
|
||||||
associations?: {
|
associations?: {
|
||||||
ack_signal: number;
|
ack_signal: number;
|
||||||
@@ -148,6 +160,11 @@ export type DeviceStatistics = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
gps?: {
|
||||||
|
elevation: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
};
|
||||||
version?: number;
|
version?: number;
|
||||||
};
|
};
|
||||||
const getLastStats = (serialNumber?: string) =>
|
const getLastStats = (serialNumber?: string) =>
|
||||||
@@ -163,7 +180,7 @@ export const useGetDeviceLastStats = ({
|
|||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
}) =>
|
}) =>
|
||||||
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
|
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '' && false,
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
@@ -183,24 +200,12 @@ export const useGetDeviceNewestStats = ({
|
|||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
}) => {
|
}) =>
|
||||||
const queryClient = useQueryClient();
|
useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
|
||||||
|
|
||||||
return useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
|
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
onSuccess: (response) => {
|
|
||||||
const entry = response.data[0];
|
|
||||||
// If we have a valid entry, we prefill lastStats, if not we trigger a fetch of the last statistics
|
|
||||||
if (entry) {
|
|
||||||
queryClient.setQueryData(['device', serialNumber, 'last-statistics'], entry.data);
|
|
||||||
} else {
|
|
||||||
queryClient.fetchQuery(['device', serialNumber, 'last-statistics']);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const getOuis = (macs?: string[]) => async () =>
|
const getOuis = (macs?: string[]) => async () =>
|
||||||
axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{
|
axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
|
|
||||||
@@ -85,14 +86,44 @@ export const useTrace = ({ serialNumber, alertOnCompletion }: { serialNumber: st
|
|||||||
export const downloadTrace = (serialNumber: string, commandId: string) =>
|
export const downloadTrace = (serialNumber: string, commandId: string) =>
|
||||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||||
|
|
||||||
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||||
useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = window.URL.createObjectURL(blob);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
link.download = `Trace_${commandId}.pcap`;
|
const headerLine =
|
||||||
|
(response.headers['content-disposition'] as string | undefined) ??
|
||||||
|
(response.headers['content-disposition'] as string | undefined);
|
||||||
|
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Trace_${commandId}.pcap`;
|
||||||
|
link.download = filename;
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
const bufferResponse = e.response?.data;
|
||||||
|
let errorMessage = '';
|
||||||
|
// If the response is a buffer, parse to JSON object
|
||||||
|
if (bufferResponse instanceof ArrayBuffer) {
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||||
|
errorMessage = json.ErrorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: `trace-download-error-${serialNumber}`,
|
||||||
|
title: t('common.error'),
|
||||||
|
description: errorMessage,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
@@ -58,12 +58,24 @@ export type User = {
|
|||||||
waitingForEmailCheck: boolean;
|
waitingForEmailCheck: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvatarPromises = (userList: User[]) => {
|
const getAvatarPromises = (userList: User[], queryClient: QueryClient) => {
|
||||||
const promises = userList.map(async (user) => {
|
const promises = userList.map(async (user) => {
|
||||||
if (user.avatar !== '' && user.avatar !== '0') {
|
if (user.avatar !== '' && user.avatar !== '0') {
|
||||||
return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
// If the avatar is already in the cache, return it
|
||||||
responseType: 'arraybuffer',
|
const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]);
|
||||||
});
|
if (cachedAvatar) return cachedAvatar;
|
||||||
|
|
||||||
|
return axiosSec
|
||||||
|
.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
queryClient.setQueryData(['avatar', user.id, user.avatar], response);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve('');
|
return Promise.resolve('');
|
||||||
});
|
});
|
||||||
@@ -71,10 +83,35 @@ const getAvatarPromises = (userList: User[]) => {
|
|||||||
return promises;
|
return promises;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsers = async () => {
|
const getBatchUsers = async (offset: number, limit: number) => {
|
||||||
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
|
const users = await axiosSec
|
||||||
|
.get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`)
|
||||||
|
.then(({ data }) => data.users as User[]);
|
||||||
|
|
||||||
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllUsers = async () => {
|
||||||
|
let users: User[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
const limit = 500;
|
||||||
|
let lastResponseLength = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await getBatchUsers(offset, limit);
|
||||||
|
users = [...users, ...response];
|
||||||
|
offset += limit;
|
||||||
|
lastResponseLength = response.length;
|
||||||
|
} while (lastResponseLength === limit);
|
||||||
|
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsers = async (queryClient: QueryClient) => {
|
||||||
|
const users = await getAllUsers();
|
||||||
|
|
||||||
|
const avatars = await Promise.allSettled(getAvatarPromises(users, queryClient)).then((results) =>
|
||||||
results.map((response) => {
|
results.map((response) => {
|
||||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||||
const base64 = btoa(
|
const base64 = btoa(
|
||||||
@@ -93,8 +130,10 @@ const getUsers = async () => {
|
|||||||
export const useGetUsers = () => {
|
export const useGetUsers = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useQuery(['users'], getUsers, {
|
return useQuery(['users'], () => getUsers(queryClient), {
|
||||||
|
staleTime: 30 * 1000,
|
||||||
onError: (e: AxiosError) => {
|
onError: (e: AxiosError) => {
|
||||||
if (!toast.isActive('users-fetching-error'))
|
if (!toast.isActive('users-fetching-error'))
|
||||||
toast({
|
toast({
|
||||||
@@ -118,7 +157,7 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
return useQuery(
|
return useQuery(
|
||||||
['get-user', id],
|
['users', id],
|
||||||
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
||||||
{
|
{
|
||||||
enabled,
|
enabled,
|
||||||
@@ -173,16 +212,41 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const useSuspendUser = ({ id }: { id: string }) =>
|
export const useSuspendUser = ({ id }: { id: string }) => {
|
||||||
useMutation((isSuspended: boolean) =>
|
const queryClient = useQueryClient();
|
||||||
axiosSec.put(`user/${id}`, {
|
|
||||||
suspended: isSuspended,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}));
|
|
||||||
|
|
||||||
export const useResetPassword = ({ id }: { id: string }) =>
|
return useMutation(
|
||||||
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
(isSuspended: boolean) =>
|
||||||
|
axiosSec.put(`user/${id}`, {
|
||||||
|
suspended: isSuspended,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetMfa = ({ id }: { id: string }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetPassword = ({ id }: { id: string }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
||||||
export const useDeleteUser = () => {
|
export const useDeleteUser = () => {
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
CircularProgressLabel,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
VStack,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ArrowSquareDown, ArrowSquareUp, Clock } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Card } from 'components/Containers/Card';
|
||||||
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
||||||
|
import { bytesString } from 'helpers/stringHelper';
|
||||||
import { useGetDevicesStats } from 'hooks/Network/Devices';
|
import { useGetDevicesStats } from 'hooks/Network/Devices';
|
||||||
|
|
||||||
const SidebarDevices = () => {
|
const SidebarDevices = () => {
|
||||||
@@ -10,18 +23,19 @@ const SidebarDevices = () => {
|
|||||||
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
||||||
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
||||||
|
|
||||||
const getTime = () => {
|
const time = React.useMemo(() => {
|
||||||
if (lastTime === undefined || lastUpdate === undefined) return null;
|
if (lastTime === undefined || lastUpdate === undefined) return null;
|
||||||
|
|
||||||
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
||||||
|
|
||||||
return Math.max(0, Math.floor(seconds / 1000));
|
return Math.max(0, Math.floor(seconds / 1000));
|
||||||
};
|
}, [lastTime, lastUpdate]);
|
||||||
|
|
||||||
const refresh = () => {
|
const circleColor = () => {
|
||||||
if (document.visibilityState !== 'hidden') {
|
if (time === null) return 'gray.300';
|
||||||
getStats.refetch();
|
if (time < 10) return 'green.300';
|
||||||
}
|
if (time < 30) return 'yellow.300';
|
||||||
|
return 'red.300';
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -37,37 +51,60 @@ const SidebarDevices = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
document.addEventListener('visibilitychange', refresh);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('visibilitychange', refresh);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!getStats.data) return null;
|
if (!getStats.data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={4}>
|
<Card borderWidth="2px">
|
||||||
<Flex flexDir="column" textAlign="center">
|
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
<CircularProgress
|
||||||
<Heading size="xs">
|
isIndeterminate
|
||||||
{t('common.connected')} {t('devices.title')}
|
color={circleColor()}
|
||||||
</Heading>
|
position="absolute"
|
||||||
<Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400">
|
right="6px"
|
||||||
({getStats.data.connectingDevices} {t('controller.devices.connecting')})
|
top="6px"
|
||||||
</Heading>
|
w="unset"
|
||||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
size={6}
|
||||||
<Heading size="md" textAlign="center" mt={2}>
|
thickness="14px"
|
||||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
>
|
||||||
|
<CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel>
|
||||||
|
</CircularProgress>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||||
|
<Box position="absolute" right="8px" top="8px" w="unset" hidden>
|
||||||
|
<Clock size={16} />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
<VStack mb={-1}>
|
||||||
|
<Flex flexDir="column" textAlign="center">
|
||||||
|
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||||
|
<Heading size="xs" display="flex" justifyContent="center">
|
||||||
|
<Text>
|
||||||
|
{t('common.connected')} {t('devices.title')}{' '}
|
||||||
|
</Text>{' '}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Tooltip>
|
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
<Heading size="md" textAlign="center" mt={1}>
|
||||||
<Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400">
|
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||||
{t('controller.stats.seconds_ago', { s: getTime() })}
|
</Heading>
|
||||||
</Heading>
|
</Tooltip>
|
||||||
</Flex>
|
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||||
</VStack>
|
<Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
|
||||||
|
<Tooltip hasArrow label="Rx">
|
||||||
|
<Flex alignItems="center" mr={1}>
|
||||||
|
<Icon as={ArrowSquareUp} weight="bold" boxSize={5} mt="1px" color="blue.400" />{' '}
|
||||||
|
{getStats.data.rx !== undefined ? bytesString(getStats.data.rx, 0) : '-'}
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip hasArrow label="Tx">
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Icon as={ArrowSquareDown} weight="bold" boxSize={5} mt="1px" color="purple.400" />{' '}
|
||||||
|
{getStats.data.tx !== undefined ? bytesString(getStats.data.tx, 0) : '-'}
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface Column<T> {
|
|||||||
alwaysShow?: boolean;
|
alwaysShow?: boolean;
|
||||||
Footer?: string;
|
Footer?: string;
|
||||||
accessor?: string;
|
accessor?: string;
|
||||||
|
stopPropagation?: boolean;
|
||||||
disableSortBy?: boolean;
|
disableSortBy?: boolean;
|
||||||
hasPopover?: boolean;
|
hasPopover?: boolean;
|
||||||
customMaxWidth?: string;
|
customMaxWidth?: string;
|
||||||
|
|||||||
@@ -99,13 +99,14 @@ const DefaultConfigurationsList = () => {
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<Box overflowX="auto" w="100%">
|
<Box overflowX="auto" w="100%">
|
||||||
<LoadingOverlay isLoading={getConfigs.isFetching}>
|
<LoadingOverlay isLoading={getConfigs.isFetching}>
|
||||||
<DataTable
|
<DataTable<DefaultConfigurationResponse>
|
||||||
columns={columns as Column<object>[]}
|
columns={columns}
|
||||||
saveSettingsId="firmware.table"
|
saveSettingsId="firmware.table"
|
||||||
data={getConfigs.data ?? []}
|
data={getConfigs.data ?? []}
|
||||||
obj={t('controller.configurations.title')}
|
obj={t('controller.configurations.title')}
|
||||||
minHeight="200px"
|
minHeight="200px"
|
||||||
sortBy={[{ id: 'name', desc: true }]}
|
sortBy={[{ id: 'name', desc: true }]}
|
||||||
|
onRowClick={onViewDetails}
|
||||||
/>
|
/>
|
||||||
</LoadingOverlay>
|
</LoadingOverlay>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
75
src/pages/Device/LocationDisplayButton.tsx
Normal file
75
src/pages/Device/LocationDisplayButton.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { Wrapper } from '@googlemaps/react-wrapper';
|
||||||
|
import { Globe } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { GoogleMap } from 'components/Maps/GoogleMap';
|
||||||
|
import { GoogleMapMarker } from 'components/Maps/GoogleMap/Marker';
|
||||||
|
import { Modal } from 'components/Modals/Modal';
|
||||||
|
import { useGetSystemSecret } from 'hooks/Network/Secrets';
|
||||||
|
import { useGetDeviceLastStats } from 'hooks/Network/Statistics';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocationDisplayButton = ({ serialNumber }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' });
|
||||||
|
const getLastStats = useGetDeviceLastStats({ serialNumber });
|
||||||
|
|
||||||
|
const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => {
|
||||||
|
if (!getLastStats.data?.gps) return undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
lat: Number.parseFloat(getLastStats.data.gps.latitude),
|
||||||
|
lng: Number.parseFloat(getLastStats.data.gps.longitude),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [getLastStats.data?.gps]);
|
||||||
|
|
||||||
|
if (!location) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
|
||||||
|
{t('locations.view_gps')}
|
||||||
|
</Button>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}>
|
||||||
|
<Box w="100%" h="100%">
|
||||||
|
<Flex mb={4}>
|
||||||
|
<FormControl w="unset">
|
||||||
|
<FormLabel>{t('locations.lat')}</FormLabel>
|
||||||
|
<pre>{location.lat}</pre>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl w="unset" mx={4}>
|
||||||
|
<FormLabel>{t('locations.longitude')}</FormLabel>
|
||||||
|
<pre>{location.lng}</pre>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl w="unset">
|
||||||
|
<FormLabel>{t('locations.elevation')}</FormLabel>
|
||||||
|
<pre>{getLastStats.data?.gps?.elevation}</pre>
|
||||||
|
</FormControl>
|
||||||
|
</Flex>
|
||||||
|
{getGoogleApiKey.data ? (
|
||||||
|
<Box h="500px">
|
||||||
|
<Wrapper apiKey={getGoogleApiKey.data.value}>
|
||||||
|
<GoogleMap center={location} style={{ flexGrow: '1', height: '100%' }} zoom={10}>
|
||||||
|
<GoogleMapMarker position={location} />
|
||||||
|
</GoogleMap>
|
||||||
|
</Wrapper>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationDisplayButton;
|
||||||
@@ -28,7 +28,7 @@ import { Modal } from 'components/Modals/Modal';
|
|||||||
import WifiScanResultDisplay from 'components/Modals/WifiScanModal/ResultDisplay';
|
import WifiScanResultDisplay from 'components/Modals/WifiScanModal/ResultDisplay';
|
||||||
import { compactDate } from 'helpers/dateFormatting';
|
import { compactDate } from 'helpers/dateFormatting';
|
||||||
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||||
import { DeviceCommandHistory } from 'hooks/Network/Commands';
|
import { DeviceCommandHistory, useGetSingleCommandHistory } from 'hooks/Network/Commands';
|
||||||
import { WifiScanResult } from 'models/Device';
|
import { WifiScanResult } from 'models/Device';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -39,9 +39,13 @@ type Props = {
|
|||||||
command?: DeviceCommandHistory;
|
command?: DeviceCommandHistory;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CommandResultModal = ({ modalProps, command }: Props) => {
|
const CommandResultModal = ({ modalProps, command: initialCommandInfo }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
const { data: command } = useGetSingleCommandHistory({
|
||||||
|
commandId: initialCommandInfo?.UUID ?? '',
|
||||||
|
serialNumber: initialCommandInfo?.serialNumber ?? '',
|
||||||
|
});
|
||||||
|
|
||||||
if (!command) return null;
|
if (!command) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ const CommandHistory = ({ serialNumber }: Props) => {
|
|||||||
<Box textAlign="right" display="flex">
|
<Box textAlign="right" display="flex">
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<HStack>
|
<HStack>
|
||||||
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
|
||||||
<ColumnPicker
|
<ColumnPicker
|
||||||
columns={columns as Column<unknown>[]}
|
columns={columns as Column<unknown>[]}
|
||||||
hiddenColumns={hiddenColumns}
|
hiddenColumns={hiddenColumns}
|
||||||
setHiddenColumns={setHiddenColumns}
|
setHiddenColumns={setHiddenColumns}
|
||||||
preference="gateway.device.commandshistory.hiddenColumns"
|
preference="gateway.device.commandshistory.hiddenColumns"
|
||||||
/>
|
/>
|
||||||
|
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
isCompact
|
isCompact
|
||||||
isFetching={getCommands.isFetching}
|
isFetching={getCommands.isFetching}
|
||||||
|
|||||||
@@ -99,16 +99,6 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
|
|||||||
const actionCell = React.useCallback(
|
const actionCell = React.useCallback(
|
||||||
(command: DeviceCommandHistory) => (
|
(command: DeviceCommandHistory) => (
|
||||||
<HStack>
|
<HStack>
|
||||||
<Tooltip label={t('common.view_details')}>
|
|
||||||
<IconButton
|
|
||||||
aria-label={t('common.view_details')}
|
|
||||||
onClick={onOpenDetails(command)}
|
|
||||||
colorScheme="blue"
|
|
||||||
icon={<MagnifyingGlass size={20} />}
|
|
||||||
size="sm"
|
|
||||||
isLoading={loadingDeleteSerial === command.UUID}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label={t('crud.delete')}>
|
<Tooltip label={t('crud.delete')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label={t('crud.delete')}
|
aria-label={t('crud.delete')}
|
||||||
@@ -119,6 +109,16 @@ const useCommandHistoryTable = ({ serialNumber, limit }: Props) => {
|
|||||||
isLoading={loadingDeleteSerial === command.UUID}
|
isLoading={loadingDeleteSerial === command.UUID}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip label={t('common.view_details')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('common.view_details')}
|
||||||
|
onClick={onOpenDetails(command)}
|
||||||
|
colorScheme="blue"
|
||||||
|
icon={<MagnifyingGlass size={20} />}
|
||||||
|
size="sm"
|
||||||
|
isLoading={loadingDeleteSerial === command.UUID}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</HStack>
|
</HStack>
|
||||||
),
|
),
|
||||||
[loadingDeleteSerial],
|
[loadingDeleteSerial],
|
||||||
|
|||||||
94
src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx
Normal file
94
src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import HistoryDatePickers from '../DatePickers';
|
||||||
|
import DeleteLogModal from './DeleteModal';
|
||||||
|
import useDeviceLogsTable from './useDeviceLogsTable';
|
||||||
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||||
|
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
|
||||||
|
import { DataTable } from 'components/DataTables/DataTable';
|
||||||
|
import { Column } from 'models/Table';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
const CrashLogs = ({ serialNumber }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [limit, setLimit] = React.useState(25);
|
||||||
|
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||||
|
const { time, setTime, getCustomLogs, getLogs, columns, modal } = useDeviceLogsTable({
|
||||||
|
serialNumber,
|
||||||
|
limit,
|
||||||
|
logType: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setNewTime = (start: Date, end: Date) => {
|
||||||
|
setTime({ start, end });
|
||||||
|
};
|
||||||
|
const onClear = () => {
|
||||||
|
setTime(undefined);
|
||||||
|
};
|
||||||
|
const raiseLimit = () => {
|
||||||
|
setLimit(limit + 25);
|
||||||
|
};
|
||||||
|
|
||||||
|
const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit;
|
||||||
|
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded);
|
||||||
|
if (getLogs.data) return getLogs.data.values;
|
||||||
|
return [];
|
||||||
|
}, [getLogs.data, getCustomLogs.data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Flex>
|
||||||
|
<Spacer />
|
||||||
|
<HStack>
|
||||||
|
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||||
|
<ColumnPicker
|
||||||
|
columns={columns as Column<unknown>[]}
|
||||||
|
hiddenColumns={hiddenColumns}
|
||||||
|
setHiddenColumns={setHiddenColumns}
|
||||||
|
preference="gateway.device.logs.hiddenColumns"
|
||||||
|
/>
|
||||||
|
<DeleteLogModal serialNumber={serialNumber} logType={0} />
|
||||||
|
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
<Box overflowY="auto" h="300px">
|
||||||
|
<DataTable
|
||||||
|
columns={
|
||||||
|
columns as {
|
||||||
|
id: string;
|
||||||
|
Header: string;
|
||||||
|
Footer: string;
|
||||||
|
accessor: string;
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
data={data}
|
||||||
|
isLoading={getLogs.isFetching || getCustomLogs.isFetching}
|
||||||
|
hiddenColumns={hiddenColumns}
|
||||||
|
obj={t('controller.devices.logs')}
|
||||||
|
// @ts-ignore
|
||||||
|
hideControls
|
||||||
|
showAllRows
|
||||||
|
/>
|
||||||
|
{getLogs.data !== undefined && (
|
||||||
|
<Center mt={1} hidden={getCustomLogs.data !== undefined}>
|
||||||
|
{!noMoreAvailable || getLogs.isFetching ? (
|
||||||
|
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}>
|
||||||
|
{t('controller.devices.show_more')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Heading size="sm">{t('controller.devices.no_more_available')}!</Heading>
|
||||||
|
)}
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{modal}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CrashLogs;
|
||||||
@@ -16,8 +16,8 @@ const CustomInputButton = React.forwardRef(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
type Props = { serialNumber: string };
|
type Props = { serialNumber: string; logType: 0 | 1 };
|
||||||
const DeleteLogModal = ({ serialNumber }: Props) => {
|
const DeleteLogModal = ({ serialNumber, logType }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const modalProps = useDisclosure();
|
const modalProps = useDisclosure();
|
||||||
@@ -26,7 +26,7 @@ const DeleteLogModal = ({ serialNumber }: Props) => {
|
|||||||
|
|
||||||
const onDeleteClick = () => {
|
const onDeleteClick = () => {
|
||||||
deleteLogs.mutate(
|
deleteLogs.mutate(
|
||||||
{ endDate: Math.floor(date.getTime() / 1000), serialNumber },
|
{ endDate: Math.floor(date.getTime() / 1000), serialNumber, logType },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
modalProps.onClose();
|
modalProps.onClose();
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Button, Code, Heading, useClipboard } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||||
|
import { Modal } from 'components/Modals/Modal';
|
||||||
|
import { DeviceLog } from 'hooks/Network/DeviceLogs';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
modalProps: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
log?: DeviceLog;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailedLogViewModal = ({ modalProps, log }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(log?.log ?? {}, null, 2));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setValue(JSON.stringify(log?.log ?? {}, null, 2));
|
||||||
|
}, [log]);
|
||||||
|
|
||||||
|
if (!log) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={modalProps.isOpen}
|
||||||
|
onClose={modalProps.onClose}
|
||||||
|
title={t('devices.logs_one')}
|
||||||
|
topRightButtons={
|
||||||
|
<Button onClick={onCopy} size="md" colorScheme="teal">
|
||||||
|
{hasCopied ? `${t('common.copied')}!` : t('common.copy')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm">
|
||||||
|
<FormattedDate date={log.recorded} />
|
||||||
|
</Heading>
|
||||||
|
<Heading size="sm">
|
||||||
|
{t('controller.devices.severity')}: {log.severity}
|
||||||
|
</Heading>
|
||||||
|
<Heading size="sm">
|
||||||
|
{t('controller.devices.config_id')}: {log.UUID}
|
||||||
|
</Heading>
|
||||||
|
<Code whiteSpace="pre-line" mt={2}>
|
||||||
|
{log.log}
|
||||||
|
</Code>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailedLogViewModal;
|
||||||
@@ -16,7 +16,7 @@ const LogHistory = ({ serialNumber }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [limit, setLimit] = React.useState(25);
|
const [limit, setLimit] = React.useState(25);
|
||||||
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||||
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit });
|
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit, logType: 0 });
|
||||||
|
|
||||||
const setNewTime = (start: Date, end: Date) => {
|
const setNewTime = (start: Date, end: Date) => {
|
||||||
setTime({ start, end });
|
setTime({ start, end });
|
||||||
@@ -48,7 +48,7 @@ const LogHistory = ({ serialNumber }: Props) => {
|
|||||||
setHiddenColumns={setHiddenColumns}
|
setHiddenColumns={setHiddenColumns}
|
||||||
preference="gateway.device.logs.hiddenColumns"
|
preference="gateway.device.logs.hiddenColumns"
|
||||||
/>
|
/>
|
||||||
<DeleteLogModal serialNumber={serialNumber} />
|
<DeleteLogModal serialNumber={serialNumber} logType={0} />
|
||||||
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
|
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box } from '@chakra-ui/react';
|
import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { MagnifyingGlass } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import DetailedLogViewModal from './DetailedLogViewModal';
|
||||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||||
import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs';
|
import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs';
|
||||||
import { Column } from 'models/Table';
|
import { Column } from 'models/Table';
|
||||||
@@ -8,18 +10,49 @@ import { Column } from 'models/Table';
|
|||||||
type Props = {
|
type Props = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
logType: 0 | 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
|
const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const getLogs = useGetDeviceLogs({ serialNumber, limit });
|
const getLogs = useGetDeviceLogs({ serialNumber, limit, logType });
|
||||||
|
const modalProps = useDisclosure();
|
||||||
|
const [log, setLog] = React.useState<DeviceLog | undefined>();
|
||||||
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
||||||
const getCustomLogs = useGetDeviceLogsWithTimestamps({
|
const getCustomLogs = useGetDeviceLogsWithTimestamps({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
|
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
|
||||||
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
|
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
|
||||||
|
logType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onOpen = React.useCallback((v: DeviceLog) => {
|
||||||
|
setLog(v);
|
||||||
|
modalProps.onOpen();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const logCell = React.useCallback(
|
||||||
|
(v: DeviceLog) =>
|
||||||
|
logType === 1 ? (
|
||||||
|
<Box display="flex">
|
||||||
|
<IconButton
|
||||||
|
aria-label="Open Log Details"
|
||||||
|
onClick={() => onOpen(v)}
|
||||||
|
colorScheme="blue"
|
||||||
|
icon={<MagnifyingGlass size={16} />}
|
||||||
|
size="xs"
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
<Text my="auto" maxW="calc(20vw)" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{v.log}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
v.log
|
||||||
|
),
|
||||||
|
[onOpen],
|
||||||
|
);
|
||||||
|
|
||||||
const dateCell = React.useCallback(
|
const dateCell = React.useCallback(
|
||||||
(v: number) => (
|
(v: number) => (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -65,6 +98,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
|
|||||||
Footer: '',
|
Footer: '',
|
||||||
accessor: 'log',
|
accessor: 'log',
|
||||||
customWidth: '35px',
|
customWidth: '35px',
|
||||||
|
Cell: (v) => logCell(v.cell.row.original),
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,6 +119,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
|
|||||||
getCustomLogs,
|
getCustomLogs,
|
||||||
time,
|
time,
|
||||||
setTime,
|
setTime,
|
||||||
|
modal: <DetailedLogViewModal modalProps={modalProps} log={log} />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import CommandHistory from './CommandHistory';
|
import CommandHistory from './CommandHistory';
|
||||||
import HealthCheckHistory from './HealthCheckHistory';
|
import HealthCheckHistory from './HealthCheckHistory';
|
||||||
import LogHistory from './LogHistory';
|
import LogHistory from './LogHistory';
|
||||||
|
import CrashLogs from './LogHistory/CrashLogs';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
|
|||||||
<Tab fontSize="lg" fontWeight="bold">
|
<Tab fontSize="lg" fontWeight="bold">
|
||||||
{t('controller.devices.logs')}
|
{t('controller.devices.logs')}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab fontSize="lg" fontWeight="bold">
|
||||||
|
{t('devices.crash_logs')}
|
||||||
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel p={0}>
|
<TabPanel p={0}>
|
||||||
@@ -51,10 +55,12 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
|
|||||||
<TabPanel>
|
<TabPanel>
|
||||||
<HealthCheckHistory serialNumber={serialNumber} />
|
<HealthCheckHistory serialNumber={serialNumber} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<LogHistory serialNumber={serialNumber} />
|
<LogHistory serialNumber={serialNumber} />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<CrashLogs serialNumber={serialNumber} />
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box, Flex, Heading, ListItem, Text, UnorderedList } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
ListItem,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagLeftIcon,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
UnorderedList,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { LockSimple, LockSimpleOpen } from 'phosphor-react';
|
import { LockSimple, LockSimpleOpen } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
@@ -20,7 +31,7 @@ const RestrictionsCard = ({ serialNumber }: Props) => {
|
|||||||
ssh: 'SSH',
|
ssh: 'SSH',
|
||||||
rtty: 'RTTY',
|
rtty: 'RTTY',
|
||||||
tty: t('restrictions.tty'),
|
tty: t('restrictions.tty'),
|
||||||
developer: t('restrictions.developer'),
|
// developer: t('restrictions.developer'),
|
||||||
upgrade: t('restrictions.signed_upgrade'),
|
upgrade: t('restrictions.signed_upgrade'),
|
||||||
commands: t('restrictions.gw_commands'),
|
commands: t('restrictions.gw_commands'),
|
||||||
} as { [key: string]: string };
|
} as { [key: string]: string };
|
||||||
@@ -38,27 +49,52 @@ const RestrictionsCard = ({ serialNumber }: Props) => {
|
|||||||
return restrictedKeys.map(([k]) => <ListItem key={k}>{LABELS[k]}</ListItem>);
|
return restrictedKeys.map(([k]) => <ListItem key={k}>{LABELS[k]}</ListItem>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isMissingSigningInfo =
|
||||||
|
!restrictions.key_info ||
|
||||||
|
(!restrictions.key_info.algo && !restrictions.key_info.vendor) ||
|
||||||
|
(restrictions.key_info.algo.length === 0 && restrictions.key_info.vendor.length === 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card mb={4}>
|
<Card mb={4}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Heading size="md">{t('restrictions.title')}</Heading>
|
<Heading size="md" my="auto" mr={2}>
|
||||||
|
{t('restrictions.title')}
|
||||||
|
</Heading>
|
||||||
|
{getDevice.data?.restrictionDetails?.developer ? (
|
||||||
|
<Tooltip label={t('devices.restricted_overriden')} hasArrow>
|
||||||
|
<Tag size="lg" colorScheme="green">
|
||||||
|
<TagLeftIcon boxSize="18px" as={LockSimpleOpen} />
|
||||||
|
<TagLabel>{t('devices.restrictions_overriden_title')}</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p={0} display="block">
|
<CardBody p={0} display="block">
|
||||||
<Flex mt={2}>
|
<Flex mt={2}>
|
||||||
<Heading size="sm" mr={2}>
|
<Heading size="sm" mr={2} my="auto">
|
||||||
{t('restrictions.countries')}:
|
{t('restrictions.countries')}:
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text>{restrictions.country.join(', ')}</Text>
|
<Text my="auto">
|
||||||
|
{restrictions.country?.length === 0 ? t('common.all') : restrictions.country.join(', ')}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Heading size="sm" mt={2}>
|
<Flex mt={2}>
|
||||||
{t('restrictions.key_verification')}
|
<Heading size="sm" mt={2} my="auto">
|
||||||
</Heading>
|
{t('restrictions.key_verification')} {isMissingSigningInfo ? ':' : ''}
|
||||||
<UnorderedList>
|
</Heading>
|
||||||
|
{isMissingSigningInfo ? (
|
||||||
|
<Text my="auto" ml={2}>
|
||||||
|
{t('common.none')}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Flex>
|
||||||
|
<UnorderedList hidden={isMissingSigningInfo}>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
{t('controller.wifi.vendor')}: {restrictions.key_info?.vendor}
|
{t('controller.wifi.vendor')}:{' '}
|
||||||
|
{restrictions.key_info?.vendor?.length > 0 ? restrictions.key_info?.vendor : '-'}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
{t('restrictions.algo')}: {restrictions.key_info?.algo}
|
{t('restrictions.algo')}: {restrictions.key_info?.algo?.length > 0 ? restrictions.key_info?.algo : '-'}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
<Flex mt={2}>
|
<Flex mt={2}>
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
|
Filler,
|
||||||
|
ChartData,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
|
||||||
|
|
||||||
const getDivisionFactor = (maxBytes: number) => {
|
const getDivisionFactor = (maxBytes: number) => {
|
||||||
if (maxBytes < 1024) {
|
if (maxBytes < 1024) {
|
||||||
@@ -42,24 +44,30 @@ const InterfaceChart = ({ data }: Props) => {
|
|||||||
|
|
||||||
const { factor, unit } = getDivisionFactor(data.maxTx);
|
const { factor, unit } = getDivisionFactor(data.maxTx);
|
||||||
|
|
||||||
const points = {
|
const points: ChartData<'line', string[], string> = {
|
||||||
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
|
labels: data.recorded.map((recorded) => new Date(recorded * 1000).toLocaleTimeString()),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
// Real 'Tx', but shown as 'Rx'
|
// Real 'Tx', but shown as 'Rx'
|
||||||
label: 'Tx',
|
label: 'Tx',
|
||||||
data: data.rx.map((tx) => Math.floor((tx / factor) * 100) / 100),
|
data: data.rx.map((tx) => (Math.floor((tx / factor) * 100) / 100).toFixed(2)),
|
||||||
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
borderColor: colorMode === 'light' ? 'rgba(99, 179, 237, 1)' : 'rgba(190, 227, 248, 1)', // blue-300 - blue-100
|
||||||
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
backgroundColor: colorMode === 'light' ? 'rgba(99, 179, 237, 0.3)' : 'rgba(190, 227, 248, 0.3)', // blue-300 - blue-100
|
||||||
},
|
tension: 0.5,
|
||||||
{
|
pointRadius: 0,
|
||||||
// Real 'Rx', but shown as 'Tx'
|
fill: 'start',
|
||||||
label: 'Rx',
|
},
|
||||||
data: data.tx.map((rx) => Math.floor((rx / factor) * 100) / 100),
|
{
|
||||||
borderColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
|
// Real 'Rx', but shown as 'Tx'
|
||||||
backgroundColor: colorMode === 'light' ? '#48BB78' : '#9AE6B4', // green-400 - green-200
|
label: 'Rx',
|
||||||
},
|
data: data.tx.map((rx) => (Math.floor((rx / factor) * 100) / 100).toFixed(2)),
|
||||||
],
|
borderColor: colorMode === 'light' ? 'rgba(72, 187, 120, 1)' : 'rgba(154, 230, 180, 1)', // green-400 - green-200
|
||||||
|
backgroundColor: colorMode === 'light' ? 'rgba(72, 187, 120, 0.3)' : 'rgba(154, 230, 180, 0.3)', // green-400 - green-200
|
||||||
|
tension: 0.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: 'start',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,20 +34,29 @@ const DeviceMemoryChart = ({ data }: Props) => {
|
|||||||
{
|
{
|
||||||
label: 'Free',
|
label: 'Free',
|
||||||
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
|
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
|
||||||
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
borderColor: colorMode === 'light' ? 'rgb(99, 179, 237, 1)' : 'rgb(190, 227, 248, 1)', // blue-300 - blue-100
|
||||||
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
backgroundColor: colorMode === 'light' ? 'rgb(99, 179, 237, 0.3)' : 'rgb(190, 227, 248, 0.3)', // blue-300 - blue-100
|
||||||
},
|
tension: 0.5,
|
||||||
{
|
pointRadius: 0,
|
||||||
label: 'Buffered',
|
fill: '+1',
|
||||||
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
|
|
||||||
borderColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
|
|
||||||
backgroundColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Cached',
|
label: 'Cached',
|
||||||
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
|
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
|
||||||
borderColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
|
borderColor: colorMode === 'light' ? 'rgb(237, 100, 166, 1)' : 'rgb(251, 182, 206, 1)', // pink-400 - pink-200
|
||||||
backgroundColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
|
backgroundColor: colorMode === 'light' ? 'rgb(237, 100, 166, 0.3)' : 'rgb(251, 182, 206, 0.3)', // pink-400 - pink-200
|
||||||
|
tension: 0.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: '+1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Buffered',
|
||||||
|
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
|
||||||
|
borderColor: colorMode === 'light' ? 'rgb(255, 240, 31, 1)' : 'rgb(250, 240, 137, 1)', // yellow-400 - yellow-200
|
||||||
|
backgroundColor: colorMode === 'light' ? 'rgb(255, 240, 31, 0.3)' : 'rgb(250, 240, 137, 0.3)', // yellow-400 - yellow-200
|
||||||
|
tension: 0.5,
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: 'origin',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const ViewLastStatsModal = ({ serialNumber }: Props) => {
|
|||||||
if (getLastStats.data) {
|
if (getLastStats.data) {
|
||||||
setValue(JSON.stringify(getLastStats.data, null, 2));
|
setValue(JSON.stringify(getLastStats.data, null, 2));
|
||||||
}
|
}
|
||||||
}, [getLastStats.data]);
|
}, [getLastStats.data, isOpen]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip label={t('statistics.last_stats')}>
|
<Tooltip label={t('statistics.last_stats')}>
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
<Heading size="md">{t('configurations.statistics')}</Heading>
|
<Heading size="md">{t('configurations.statistics')}</Heading>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<HStack>
|
<HStack>
|
||||||
<ViewLastStatsModal serialNumber={serialNumber} />
|
|
||||||
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
|
||||||
<Select value={selected} onChange={onSelectInterface}>
|
<Select value={selected} onChange={onSelectInterface}>
|
||||||
{parsedData?.interfaces
|
{parsedData?.interfaces
|
||||||
? Object.keys(parsedData.interfaces).map((v) => (
|
? Object.keys(parsedData.interfaces).map((v) => (
|
||||||
@@ -64,6 +62,8 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
: null}
|
: null}
|
||||||
<option value="memory">{t('statistics.memory')}</option>
|
<option value="memory">{t('statistics.memory')}</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||||
|
<ViewLastStatsModal serialNumber={serialNumber} />
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Props = {
|
|||||||
export const useStatisticsCard = ({ serialNumber }: Props) => {
|
export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||||
const [selected, setSelected] = React.useState('memory');
|
const [selected, setSelected] = React.useState('memory');
|
||||||
const [progress, setProgress] = React.useState(0);
|
const [progress, setProgress] = React.useState(0);
|
||||||
|
const [hasSelectedNew, setHasSelectedNew] = React.useState(false);
|
||||||
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
||||||
const onProgressChange = React.useCallback((newProgress: number) => {
|
const onProgressChange = React.useCallback((newProgress: number) => {
|
||||||
setProgress(newProgress);
|
setProgress(newProgress);
|
||||||
@@ -29,13 +30,17 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setHasSelectedNew(true);
|
||||||
setSelected(event.target.value);
|
setSelected(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsedData = React.useMemo(() => {
|
const parsedData = React.useMemo(() => {
|
||||||
if (!getStats.data && !getCustomStats.data) return undefined;
|
if (!getStats.data && !getCustomStats.data) return undefined;
|
||||||
|
|
||||||
const data: Record<string, { tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number }> = {};
|
const data: Record<
|
||||||
|
string,
|
||||||
|
{ tx: number[]; rx: number[]; recorded: number[]; maxRx: number; maxTx: number; removed?: boolean }
|
||||||
|
> = {};
|
||||||
const memoryData = {
|
const memoryData = {
|
||||||
used: [] as number[],
|
used: [] as number[],
|
||||||
buffered: [] as number[],
|
buffered: [] as number[],
|
||||||
@@ -56,7 +61,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
let updated = false;
|
let updated = false;
|
||||||
for (const inter of stat.data.interfaces ?? []) {
|
for (const inter of stat.data.interfaces ?? []) {
|
||||||
if (!updated && selected === 'memory') {
|
if (!hasSelectedNew && !updated && selected === 'memory') {
|
||||||
updated = true;
|
updated = true;
|
||||||
setSelected(inter.name);
|
setSelected(inter.name);
|
||||||
}
|
}
|
||||||
@@ -77,7 +82,10 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
let rx = inter.counters?.rx_bytes ?? 0;
|
let rx = inter.counters?.rx_bytes ?? 0;
|
||||||
let tx = inter.counters?.tx_bytes ?? 0;
|
let tx = inter.counters?.tx_bytes ?? 0;
|
||||||
|
|
||||||
if (isInterUpstream) {
|
if (inter['counters-aggregate']) {
|
||||||
|
rx = inter['counters-aggregate'].rx_bytes;
|
||||||
|
tx = inter['counters-aggregate'].tx_bytes;
|
||||||
|
} else if (isInterUpstream) {
|
||||||
for (const ssid of inter.ssids ?? []) {
|
for (const ssid of inter.ssids ?? []) {
|
||||||
rx += ssid.counters?.rx_bytes ?? 0;
|
rx += ssid.counters?.rx_bytes ?? 0;
|
||||||
tx += ssid.counters?.tx_bytes ?? 0;
|
tx += ssid.counters?.tx_bytes ?? 0;
|
||||||
@@ -97,6 +105,18 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
maxRx: rxDelta,
|
maxRx: rxDelta,
|
||||||
};
|
};
|
||||||
else {
|
else {
|
||||||
|
if (data[inter.name] && !data[inter.name]?.removed && data[inter.name]?.recorded.length === 1) {
|
||||||
|
data[inter.name]?.tx.shift();
|
||||||
|
data[inter.name]?.rx.shift();
|
||||||
|
data[inter.name]?.recorded.shift();
|
||||||
|
// @ts-ignore
|
||||||
|
data[inter.name].maxRx = rxDelta;
|
||||||
|
// @ts-ignore
|
||||||
|
data[inter.name].maxTx = txDelta;
|
||||||
|
// @ts-ignore
|
||||||
|
data[inter.name].removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
data[inter.name]?.rx.push(rxDelta);
|
data[inter.name]?.rx.push(rxDelta);
|
||||||
data[inter.name]?.tx.push(txDelta);
|
data[inter.name]?.tx.push(txDelta);
|
||||||
data[inter.name]?.recorded.push(stat.recorded);
|
data[inter.name]?.recorded.push(stat.recorded);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
|
import { Box, Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
|
||||||
import ReactCountryFlag from 'react-country-flag';
|
import ReactCountryFlag from 'react-country-flag';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LocationDisplayButton from './LocationDisplayButton';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||||
@@ -90,11 +91,12 @@ const DeviceSummary = ({ serialNumber }: Props) => {
|
|||||||
{!getDevice.data?.locale || getDevice.data?.locale === '' ? (
|
{!getDevice.data?.locale || getDevice.data?.locale === '' ? (
|
||||||
'-'
|
'-'
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Box mr={2}>
|
||||||
<ReactCountryFlag style={ICON_STYLE} countryCode={getDevice.data.locale} svg />
|
<ReactCountryFlag style={ICON_STYLE} countryCode={getDevice.data.locale} svg />
|
||||||
{COUNTRY_LIST.find(({ value }) => value === getDevice.data.locale)?.label}
|
{COUNTRY_LIST.find(({ value }) => value === getDevice.data.locale)?.label}
|
||||||
</>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
<LocationDisplayButton serialNumber={serialNumber} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem colSpan={1} alignContent="center" alignItems="center">
|
<GridItem colSpan={1} alignContent="center" alignItems="center">
|
||||||
<Heading size="sm">{t('analytics.last_contact')}:</Heading>
|
<Heading size="sm">{t('analytics.last_contact')}:</Heading>
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Heading,
|
Heading,
|
||||||
|
IconButton,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
useClipboard,
|
useClipboard,
|
||||||
useColorMode,
|
useColorMode,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { JsonViewer } from '@textea/json-viewer';
|
import { JsonViewer } from '@textea/json-viewer';
|
||||||
|
import { ListDashes } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||||
@@ -43,9 +46,15 @@ const ViewCapabilitiesModal = ({ serialNumber }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={onOpen} colorScheme="pink" mr={2}>
|
<Tooltip label={t('controller.devices.capabilities')} hasArrow>
|
||||||
{t('controller.devices.capabilities')}
|
<IconButton
|
||||||
</Button>
|
aria-label={t('controller.devices.capabilities')}
|
||||||
|
icon={<ListDashes size={20} />}
|
||||||
|
onClick={onOpen}
|
||||||
|
colorScheme="pink"
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={t('controller.devices.capabilities')}
|
title={t('controller.devices.capabilities')}
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import {
|
|||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
useClipboard,
|
useClipboard,
|
||||||
useColorMode,
|
useColorMode,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { JsonViewer } from '@textea/json-viewer';
|
import { JsonViewer } from '@textea/json-viewer';
|
||||||
|
import { Barcode } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from 'components/Modals/Modal';
|
import { Modal } from 'components/Modals/Modal';
|
||||||
import { DeviceConfiguration } from 'models/Device';
|
import { DeviceConfiguration } from 'models/Device';
|
||||||
@@ -30,9 +33,14 @@ const ViewConfigurationModal = ({ configuration }: { configuration?: DeviceConfi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={onOpen} isDisabled={!configuration} colorScheme="purple">
|
<Tooltip label={t('configurations.one')} hasArrow>
|
||||||
{t('configurations.one')}
|
<IconButton
|
||||||
</Button>
|
aria-label={t('configurations.one')}
|
||||||
|
icon={<Barcode size={20} />}
|
||||||
|
onClick={onOpen}
|
||||||
|
colorScheme="purple"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={t('configurations.one')}
|
title={t('configurations.one')}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogOverlay,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Heading,
|
Heading,
|
||||||
HStack,
|
HStack,
|
||||||
Portal,
|
Portal,
|
||||||
@@ -12,10 +19,13 @@ import {
|
|||||||
useBreakpoint,
|
useBreakpoint,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Heart, HeartBreak, LockSimple, WifiHigh, WifiSlash } from 'phosphor-react';
|
import axios from 'axios';
|
||||||
|
import { Heart, HeartBreak, LockSimple, LockSimpleOpen, WifiHigh, WifiSlash } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Masonry from 'react-masonry-css';
|
import Masonry from 'react-masonry-css';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import DeviceDetails from './Details';
|
import DeviceDetails from './Details';
|
||||||
import DeviceLogsCard from './LogsCard';
|
import DeviceLogsCard from './LogsCard';
|
||||||
import DeviceNotes from './Notes';
|
import DeviceNotes from './Notes';
|
||||||
@@ -23,6 +33,7 @@ import RestrictionsCard from './RestrictionsCard';
|
|||||||
import DeviceStatisticsCard from './StatisticsCard';
|
import DeviceStatisticsCard from './StatisticsCard';
|
||||||
import DeviceSummary from './Summary';
|
import DeviceSummary from './Summary';
|
||||||
import WifiAnalysisCard from './WifiAnalysis';
|
import WifiAnalysisCard from './WifiAnalysis';
|
||||||
|
import { DeleteButton } from 'components/Buttons/DeleteButton';
|
||||||
import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown';
|
import DeviceActionDropdown from 'components/Buttons/DeviceActionDropdown';
|
||||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
@@ -33,11 +44,12 @@ import { ConfigureModal } from 'components/Modals/ConfigureModal';
|
|||||||
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
||||||
import FactoryResetModal from 'components/Modals/FactoryResetModal';
|
import FactoryResetModal from 'components/Modals/FactoryResetModal';
|
||||||
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
|
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
|
||||||
|
import { RebootModal } from 'components/Modals/RebootModal';
|
||||||
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
||||||
import { TelemetryModal } from 'components/Modals/TelemetryModal';
|
import { TelemetryModal } from 'components/Modals/TelemetryModal';
|
||||||
import { TraceModal } from 'components/Modals/TraceModal';
|
import { TraceModal } from 'components/Modals/TraceModal';
|
||||||
import { WifiScanModal } from 'components/Modals/WifiScanModal';
|
import { WifiScanModal } from 'components/Modals/WifiScanModal';
|
||||||
import { useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices';
|
import { useDeleteDevice, useGetDevice, useGetDeviceHealthChecks, useGetDeviceStatus } from 'hooks/Network/Devices';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
@@ -45,10 +57,17 @@ type Props = {
|
|||||||
|
|
||||||
const DevicePageWrapper = ({ serialNumber }: Props) => {
|
const DevicePageWrapper = ({ serialNumber }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
const cancelRef = React.useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { mutateAsync: deleteDevice, isLoading: isDeleting } = useDeleteDevice({
|
||||||
|
serialNumber,
|
||||||
|
});
|
||||||
const getDevice = useGetDevice({ serialNumber });
|
const getDevice = useGetDevice({ serialNumber });
|
||||||
const getStatus = useGetDeviceStatus({ serialNumber });
|
const getStatus = useGetDeviceStatus({ serialNumber });
|
||||||
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 });
|
const getHealth = useGetDeviceHealthChecks({ serialNumber, limit: 1 });
|
||||||
|
const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();
|
||||||
const scanModalProps = useDisclosure();
|
const scanModalProps = useDisclosure();
|
||||||
const resetModalProps = useDisclosure();
|
const resetModalProps = useDisclosure();
|
||||||
const eventQueueProps = useDisclosure();
|
const eventQueueProps = useDisclosure();
|
||||||
@@ -56,7 +75,40 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
const upgradeModalProps = useDisclosure();
|
const upgradeModalProps = useDisclosure();
|
||||||
const telemetryModalProps = useDisclosure();
|
const telemetryModalProps = useDisclosure();
|
||||||
const traceModalProps = useDisclosure();
|
const traceModalProps = useDisclosure();
|
||||||
|
const rebootModalProps = useDisclosure();
|
||||||
const scriptModal = useScriptModal();
|
const scriptModal = useScriptModal();
|
||||||
|
// Sticky-top styles
|
||||||
|
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||||
|
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
||||||
|
|
||||||
|
const handleDeleteClick = () =>
|
||||||
|
deleteDevice(serialNumber, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
id: `delete-device-success-${serialNumber}`,
|
||||||
|
title: t('common.success'),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
navigate('/devices');
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
toast({
|
||||||
|
id: `delete-device-error-${serialNumber}`,
|
||||||
|
title: t('common.error'),
|
||||||
|
description: e.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const connectedTag = React.useMemo(() => {
|
const connectedTag = React.useMemo(() => {
|
||||||
if (!getStatus.data) return null;
|
if (!getStatus.data) return null;
|
||||||
|
|
||||||
@@ -100,9 +152,28 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
);
|
);
|
||||||
}, [getStatus.data, getHealth.data]);
|
}, [getStatus.data, getHealth.data]);
|
||||||
|
|
||||||
// Sticky-top styles
|
const restrictedTag = React.useMemo(() => {
|
||||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
if (!getDevice.data || !getDevice.data.restrictedDevice) return null;
|
||||||
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
|
||||||
|
if (getDevice.data.restrictionDetails?.developer)
|
||||||
|
return (
|
||||||
|
<Tooltip label={t('devices.restricted_overriden')} hasArrow>
|
||||||
|
<Tag size="lg" colorScheme="green">
|
||||||
|
<TagLeftIcon boxSize="18px" as={LockSimpleOpen} />
|
||||||
|
<TagLabel>
|
||||||
|
{t('devices.restricted')} {isCompact ? '' : '(Dev Mode)'}
|
||||||
|
</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag size="lg" colorScheme="red">
|
||||||
|
<TagLeftIcon boxSize="18px" as={LockSimple} />
|
||||||
|
<TagLabel>{t('devices.restricted')}</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}, [getDevice.data, isCompact]);
|
||||||
|
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
getDevice.refetch();
|
getDevice.refetch();
|
||||||
@@ -119,16 +190,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
<Heading size="md">{serialNumber}</Heading>
|
<Heading size="md">{serialNumber}</Heading>
|
||||||
{connectedTag}
|
{connectedTag}
|
||||||
{healthTag}
|
{healthTag}
|
||||||
{getDevice.data?.restrictedDevice && (
|
{restrictedTag}
|
||||||
<Tag size="lg" colorScheme="gray">
|
|
||||||
<TagLeftIcon boxSize="18px" as={LockSimple} />
|
|
||||||
<TagLabel>{t('devices.restricted')}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
{breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />}
|
{breakpoint !== 'base' && breakpoint !== 'md' && <DeviceSearchBar />}
|
||||||
|
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||||
{getDevice?.data && (
|
{getDevice?.data && (
|
||||||
<DeviceActionDropdown
|
<DeviceActionDropdown
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -142,6 +209,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
onOpenConfigureModal={configureModalProps.onOpen}
|
onOpenConfigureModal={configureModalProps.onOpen}
|
||||||
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
||||||
onOpenScriptModal={scriptModal.openModal}
|
onOpenScriptModal={scriptModal.openModal}
|
||||||
|
onOpenRebootModal={rebootModalProps.onOpen}
|
||||||
size="md"
|
size="md"
|
||||||
isCompact
|
isCompact
|
||||||
/>
|
/>
|
||||||
@@ -172,16 +240,12 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
<Heading size="md">{serialNumber}</Heading>
|
<Heading size="md">{serialNumber}</Heading>
|
||||||
{connectedTag}
|
{connectedTag}
|
||||||
{healthTag}
|
{healthTag}
|
||||||
{getDevice.data?.restrictedDevice && (
|
{restrictedTag}
|
||||||
<Tag size="lg" colorScheme="gray">
|
|
||||||
<TagLeftIcon boxSize="18px" as={LockSimple} />
|
|
||||||
<TagLabel>{t('devices.restricted')}</TagLabel>
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</HStack>
|
</HStack>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<DeviceSearchBar />
|
<DeviceSearchBar />
|
||||||
|
<DeleteButton isCompact onClick={onDeleteOpen} />
|
||||||
{getDevice?.data && (
|
{getDevice?.data && (
|
||||||
<DeviceActionDropdown
|
<DeviceActionDropdown
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -194,8 +258,10 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
onOpenEventQueue={eventQueueProps.onOpen}
|
onOpenEventQueue={eventQueueProps.onOpen}
|
||||||
onOpenConfigureModal={configureModalProps.onOpen}
|
onOpenConfigureModal={configureModalProps.onOpen}
|
||||||
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
onOpenTelemetryModal={telemetryModalProps.onOpen}
|
||||||
|
onOpenRebootModal={rebootModalProps.onOpen}
|
||||||
onOpenScriptModal={scriptModal.openModal}
|
onOpenScriptModal={scriptModal.openModal}
|
||||||
size="md"
|
size="md"
|
||||||
|
isCompact
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
@@ -210,6 +276,24 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
</Card>
|
</Card>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
|
<AlertDialog isOpen={isDeleteOpen} leastDestructiveRef={cancelRef} onClose={onDeleteClose}>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
{t('crud.delete')} {serialNumber}
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogBody>{t('crud.delete_confirm', { obj: t('devices.one') })}</AlertDialogBody>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button colorScheme="gray" mr="1" onClick={onDeleteClose} ref={cancelRef}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={isDeleting}>
|
||||||
|
{t('common.yes')}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
|
||||||
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
<FirmwareUpgradeModal modalProps={upgradeModalProps} serialNumber={serialNumber} />
|
||||||
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
<FactoryResetModal modalProps={resetModalProps} serialNumber={serialNumber} />
|
||||||
@@ -217,6 +301,7 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
|
|||||||
<EventQueueModal serialNumber={serialNumber} modalProps={eventQueueProps} />
|
<EventQueueModal serialNumber={serialNumber} modalProps={eventQueueProps} />
|
||||||
<ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} />
|
<ConfigureModal serialNumber={serialNumber} modalProps={configureModalProps} />
|
||||||
<TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} />
|
<TelemetryModal serialNumber={serialNumber} modalProps={telemetryModalProps} />
|
||||||
|
<RebootModal serialNumber={serialNumber} modalProps={rebootModalProps} />
|
||||||
{scriptModal.modal}
|
{scriptModal.modal}
|
||||||
<Box mt={isCompact ? '0px' : '68px'}>
|
<Box mt={isCompact ? '0px' : '68px'}>
|
||||||
<Masonry
|
<Masonry
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import { FormControl, FormErrorMessage, FormLabel, Input, Textarea, useDisclosure, useToast } from '@chakra-ui/react';
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertIcon,
|
|
||||||
AlertTitle,
|
|
||||||
Box,
|
|
||||||
FormControl,
|
|
||||||
FormErrorMessage,
|
|
||||||
FormLabel,
|
|
||||||
Input,
|
|
||||||
Textarea,
|
|
||||||
useDisclosure,
|
|
||||||
useToast,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CreateButton } from 'components/Buttons/CreateButton';
|
import { CreateButton } from 'components/Buttons/CreateButton';
|
||||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||||
import { Modal } from 'components/Modals/Modal';
|
import { Modal } from 'components/Modals/Modal';
|
||||||
import { useCreateBlacklist } from 'hooks/Network/Blacklist';
|
import { useCreateBlacklist } from 'hooks/Network/Blacklist';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
const CreateBlacklistModal = () => {
|
const CreateBlacklistModal = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const initialRef = React.useRef<HTMLInputElement>(null);
|
||||||
const modalProps = useDisclosure();
|
const modalProps = useDisclosure();
|
||||||
const createDevice = useCreateBlacklist();
|
const createDevice = useCreateBlacklist();
|
||||||
const [serialNumber, setSerialNumber] = React.useState<string>('');
|
const [serialNumber, setSerialNumber] = React.useState<string>('');
|
||||||
@@ -43,41 +32,50 @@ const CreateBlacklistModal = () => {
|
|||||||
});
|
});
|
||||||
modalProps.onClose();
|
modalProps.onClose();
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
id: 'add-blacklist-error',
|
||||||
|
title: t('common.error'),
|
||||||
|
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSerialValid = serialNumber.length === 12 && serialNumber.match('^[a-fA-F0-9]+$') !== null;
|
const isSerialValid = serialNumber.length === 12 && serialNumber.match('^[a-fA-F0-9]+$') !== null;
|
||||||
|
|
||||||
React.useEffect(() => {
|
const onOpen = () => {
|
||||||
setSerialNumber('');
|
setSerialNumber('');
|
||||||
setReason('');
|
setReason('');
|
||||||
}, [modalProps.isOpen]);
|
modalProps.onOpen();
|
||||||
|
setTimeout(() => {
|
||||||
|
initialRef.current?.focus();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateButton onClick={modalProps.onOpen} isCompact ml={2} />
|
<CreateButton onClick={onOpen} isCompact ml={2} />
|
||||||
<Modal
|
<Modal
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
title={t('controller.devices.add_blacklist')}
|
title={t('controller.devices.add_blacklist')}
|
||||||
topRightButtons={<SaveButton onClick={onSave} isLoading={createDevice.isLoading} isCompact />}
|
topRightButtons={<SaveButton onClick={onSave} isLoading={createDevice.isLoading} isCompact />}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{createDevice.error && (
|
|
||||||
<Alert status="error" mb={4}>
|
|
||||||
<AlertIcon />
|
|
||||||
<Box>
|
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
|
||||||
{
|
|
||||||
// @ts-ignore
|
|
||||||
<AlertDescription>{createDevice.error?.response?.data?.ErrorDescription}</AlertDescription>
|
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<FormControl isInvalid={!isSerialValid} mb={2}>
|
<FormControl isInvalid={!isSerialValid} mb={2}>
|
||||||
<FormLabel>{t('inventory.serial_number')}</FormLabel>
|
<FormLabel>{t('inventory.serial_number')}</FormLabel>
|
||||||
<Input type="text" onChange={(e) => setSerialNumber(e.target.value)} value={serialNumber} w="140px" />
|
<Input
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setSerialNumber(e.target.value)}
|
||||||
|
value={serialNumber}
|
||||||
|
w="140px"
|
||||||
|
ref={initialRef}
|
||||||
|
/>
|
||||||
<FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage>
|
<FormErrorMessage>{t('inventory.invalid_serial_number')}</FormErrorMessage>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useColorMode } from '@chakra-ui/react';
|
import { CopyIcon } from '@chakra-ui/icons';
|
||||||
|
import {
|
||||||
|
useColorMode,
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertTitle,
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
Heading,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
ListItem,
|
||||||
|
Spinner,
|
||||||
|
UnorderedList,
|
||||||
|
useClipboard,
|
||||||
|
Tooltip as ChakraTooltip,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@@ -11,20 +28,57 @@ import {
|
|||||||
Legend,
|
Legend,
|
||||||
ChartData,
|
ChartData,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
|
ChartTypeRegistry,
|
||||||
|
ScatterDataPoint,
|
||||||
|
BubbleDataPoint,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { Pie } from 'react-chartjs-2';
|
import { Pie, getElementAtEvent } from 'react-chartjs-2';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
|
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
|
||||||
|
import { Modal } from 'components/Modals/Modal';
|
||||||
import { ControllerDashboardHealth } from 'hooks/Network/Controller';
|
import { ControllerDashboardHealth } from 'hooks/Network/Controller';
|
||||||
|
import { useGetDevicesWithHealthBetween } from 'hooks/Network/HealthChecks';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
|
const LABEL_TO_LIMITS = {
|
||||||
|
'100%': { lowerLimit: 100, upperLimit: 100, label: 'With 100% Health' },
|
||||||
|
'>90%': { lowerLimit: 90, upperLimit: 99, label: 'Between 90% and 99%' },
|
||||||
|
'>60%': { lowerLimit: 60, upperLimit: 89, label: 'Between 60% and 89%' },
|
||||||
|
'<=60%': { lowerLimit: 0, upperLimit: 59, label: 'Between 0% and 59%' },
|
||||||
|
} as const;
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement);
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: ControllerDashboardHealth[];
|
data: ControllerDashboardHealth[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const OverallHealthPieChart = ({ data }: Props) => {
|
const OverallHealthPieChart = ({ data }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
const { hasCopied, onCopy, setValue } = useClipboard('');
|
||||||
|
const modalProps = useDisclosure();
|
||||||
|
const [deviceCategory, setDeviceCategory] = React.useState<{ lowerLimit: number; upperLimit: number; label: string }>(
|
||||||
|
LABEL_TO_LIMITS['100%'],
|
||||||
|
);
|
||||||
|
const serialNumbersFromCategory = useGetDevicesWithHealthBetween(deviceCategory);
|
||||||
|
const chartRef =
|
||||||
|
React.useRef<ChartJS<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (chartRef.current) {
|
||||||
|
const element = getElementAtEvent(chartRef.current, event)?.[0];
|
||||||
|
if (element && element.index !== undefined) {
|
||||||
|
const label = chartRef.current?.data?.labels?.[element.index] as keyof typeof LABEL_TO_LIMITS | undefined;
|
||||||
|
if (label && LABEL_TO_LIMITS[label]) {
|
||||||
|
setDeviceCategory(LABEL_TO_LIMITS[label]);
|
||||||
|
modalProps.onOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => {
|
const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => {
|
||||||
const totalDevices = data.reduce(
|
const totalDevices = data.reduce(
|
||||||
@@ -85,7 +139,7 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
|||||||
}
|
}
|
||||||
if (totalDevices['<60%'] > 0) {
|
if (totalDevices['<60%'] > 0) {
|
||||||
newData.push(totalDevices['<60%']);
|
newData.push(totalDevices['<60%']);
|
||||||
labels.push('<60%');
|
labels.push('<=60%');
|
||||||
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
|
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
|
||||||
backgroundColor.push(color);
|
backgroundColor.push(color);
|
||||||
borderColor.push(color);
|
borderColor.push(color);
|
||||||
@@ -105,38 +159,108 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [data, colorMode]);
|
}, [data, colorMode]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (serialNumbersFromCategory.data) setValue(serialNumbersFromCategory.data.join(','));
|
||||||
|
}, [serialNumbersFromCategory.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GraphStatDisplay
|
<>
|
||||||
title={t('controller.dashboard.overall_health')}
|
<GraphStatDisplay
|
||||||
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
title={t('controller.dashboard.overall_health')}
|
||||||
chart={
|
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
||||||
<Pie
|
chart={
|
||||||
data={parsedData}
|
<Pie
|
||||||
options={{
|
// @ts-ignore
|
||||||
plugins: {
|
ref={chartRef}
|
||||||
legend: {
|
data={parsedData}
|
||||||
position: 'top' as const,
|
onClick={onClick}
|
||||||
labels: {
|
options={{
|
||||||
color: colorMode === 'dark' ? 'white' : undefined,
|
onHover: (e, elements) => {
|
||||||
|
const element = e.native?.target as unknown as { style: { cursor: string } };
|
||||||
|
if (element && elements.length > 0) {
|
||||||
|
element.style.cursor = 'pointer';
|
||||||
|
} else if (element) {
|
||||||
|
element.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'top' as const,
|
||||||
|
labels: {
|
||||||
|
color: colorMode === 'dark' ? 'white' : undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) =>
|
||||||
|
`${context.label}: ${context.formattedValue} (${Math.round(
|
||||||
|
// @ts-ignore
|
||||||
|
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
||||||
|
)}%)`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
}}
|
||||||
display: false,
|
/>
|
||||||
},
|
}
|
||||||
tooltip: {
|
/>
|
||||||
callbacks: {
|
<Modal
|
||||||
label: (context) =>
|
title={t('controller.dashboard.overall_health')}
|
||||||
`${context.label}: ${context.formattedValue} (${Math.round(
|
{...modalProps}
|
||||||
// @ts-ignore
|
options={{
|
||||||
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
modalSize: 'sm',
|
||||||
)}%)`,
|
}}
|
||||||
},
|
topRightButtons={
|
||||||
},
|
<ChakraTooltip label={hasCopied ? `${t('common.copied')}!` : t('common.copy')} hasArrow closeOnClick={false}>
|
||||||
},
|
<IconButton
|
||||||
}}
|
aria-label={t('common.copy')}
|
||||||
/>
|
icon={<CopyIcon h={5} w={5} />}
|
||||||
}
|
onClick={onCopy}
|
||||||
/>
|
colorScheme="teal"
|
||||||
|
hidden={!serialNumbersFromCategory.data || serialNumbersFromCategory.data.length === 0}
|
||||||
|
/>
|
||||||
|
</ChakraTooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{serialNumbersFromCategory.isFetching ? (
|
||||||
|
<Center my={8}>
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
{serialNumbersFromCategory.error ? (
|
||||||
|
<Alert mb={4} status="error">
|
||||||
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{(serialNumbersFromCategory.error as AxiosError).response?.data.ErrorDescription}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
{serialNumbersFromCategory.data ? (
|
||||||
|
<Box>
|
||||||
|
<Heading size="md" mb={4}>
|
||||||
|
{serialNumbersFromCategory.data.length} {t('devices.title')} {deviceCategory.label}
|
||||||
|
</Heading>
|
||||||
|
<Box maxH="70vh" overflowY="auto" overflowX="hidden">
|
||||||
|
<UnorderedList pl={2}>
|
||||||
|
{serialNumbersFromCategory.data
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((device) => (
|
||||||
|
<ListItem key={device} fontFamily="mono">
|
||||||
|
<Link href={`#/devices/${device}`}>{device}</Link>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</UnorderedList>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface Props {
|
|||||||
onOpenConfigureModal: (serialNumber: string) => void;
|
onOpenConfigureModal: (serialNumber: string) => void;
|
||||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||||
|
onOpenRebootModal: (serialNumber: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Actions: React.FC<Props> = ({
|
const Actions: React.FC<Props> = ({
|
||||||
@@ -47,6 +48,7 @@ const Actions: React.FC<Props> = ({
|
|||||||
onOpenConfigureModal,
|
onOpenConfigureModal,
|
||||||
onOpenTelemetryModal,
|
onOpenTelemetryModal,
|
||||||
onOpenScriptModal,
|
onOpenScriptModal,
|
||||||
|
onOpenRebootModal,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
@@ -102,6 +104,7 @@ const Actions: React.FC<Props> = ({
|
|||||||
onOpenConfigureModal={onOpenConfigureModal}
|
onOpenConfigureModal={onOpenConfigureModal}
|
||||||
onOpenTelemetryModal={onOpenTelemetryModal}
|
onOpenTelemetryModal={onOpenTelemetryModal}
|
||||||
onOpenScriptModal={onOpenScriptModal}
|
onOpenScriptModal={onOpenScriptModal}
|
||||||
|
onOpenRebootModal={onOpenRebootModal}
|
||||||
/>
|
/>
|
||||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||||
<Link href={`#/devices/${device.serialNumber}`}>
|
<Link href={`#/devices/${device.serialNumber}`}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box, Button, Heading, Image, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
import { Box, Heading, Image, Link, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||||
import { LockSimple } from 'phosphor-react';
|
import { LockSimple } from 'phosphor-react';
|
||||||
import ReactCountryFlag from 'react-country-flag';
|
import ReactCountryFlag from 'react-country-flag';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -21,6 +21,7 @@ import { ConfigureModal } from 'components/Modals/ConfigureModal';
|
|||||||
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
import { EventQueueModal } from 'components/Modals/EventQueueModal';
|
||||||
import FactoryResetModal from 'components/Modals/FactoryResetModal';
|
import FactoryResetModal from 'components/Modals/FactoryResetModal';
|
||||||
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
|
import { FirmwareUpgradeModal } from 'components/Modals/FirmwareUpgradeModal';
|
||||||
|
import { RebootModal } from 'components/Modals/RebootModal';
|
||||||
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
import { useScriptModal } from 'components/Modals/ScriptModal/useScriptModal';
|
||||||
import { TelemetryModal } from 'components/Modals/TelemetryModal';
|
import { TelemetryModal } from 'components/Modals/TelemetryModal';
|
||||||
import { TraceModal } from 'components/Modals/TraceModal';
|
import { TraceModal } from 'components/Modals/TraceModal';
|
||||||
@@ -60,6 +61,7 @@ const DeviceListCard = () => {
|
|||||||
const eventQueueProps = useDisclosure();
|
const eventQueueProps = useDisclosure();
|
||||||
const telemetryModalProps = useDisclosure();
|
const telemetryModalProps = useDisclosure();
|
||||||
const configureModalProps = useDisclosure();
|
const configureModalProps = useDisclosure();
|
||||||
|
const rebootModalProps = useDisclosure();
|
||||||
const scriptModal = useScriptModal();
|
const scriptModal = useScriptModal();
|
||||||
const getCount = useGetDeviceCount({ enabled: true });
|
const getCount = useGetDeviceCount({ enabled: true });
|
||||||
const getDevices = useGetDevices({
|
const getDevices = useGetDevices({
|
||||||
@@ -98,9 +100,9 @@ const DeviceListCard = () => {
|
|||||||
setSerialNumber(serial);
|
setSerialNumber(serial);
|
||||||
configureModalProps.onOpen();
|
configureModalProps.onOpen();
|
||||||
};
|
};
|
||||||
|
const onOpenReboot = (serial: string) => {
|
||||||
const goToSerial = (serial: string) => () => {
|
setSerialNumber(serial);
|
||||||
navigate(`/devices/${serial}`);
|
rebootModalProps.onOpen();
|
||||||
};
|
};
|
||||||
|
|
||||||
const badgeCell = React.useCallback(
|
const badgeCell = React.useCallback(
|
||||||
@@ -160,9 +162,9 @@ const DeviceListCard = () => {
|
|||||||
|
|
||||||
const serialCell = React.useCallback(
|
const serialCell = React.useCallback(
|
||||||
(device: DeviceWithStatus) => (
|
(device: DeviceWithStatus) => (
|
||||||
<Button variant="link" onClick={goToSerial(device.serialNumber)} fontSize="sm">
|
<Link href={`#/devices/${device.serialNumber}`} fontSize="sm" my="auto" pt={1}>
|
||||||
<pre>{device.serialNumber}</pre>
|
<pre>{device.serialNumber}</pre>
|
||||||
</Button>
|
</Link>
|
||||||
),
|
),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
@@ -216,6 +218,7 @@ const DeviceListCard = () => {
|
|||||||
onOpenConfigureModal={onOpenConfigure}
|
onOpenConfigureModal={onOpenConfigure}
|
||||||
onOpenTelemetryModal={onOpenTelemetry}
|
onOpenTelemetryModal={onOpenTelemetry}
|
||||||
onOpenScriptModal={scriptModal.openModal}
|
onOpenScriptModal={scriptModal.openModal}
|
||||||
|
onOpenRebootModal={onOpenReboot}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[],
|
[],
|
||||||
@@ -251,6 +254,7 @@ const DeviceListCard = () => {
|
|||||||
Footer: '',
|
Footer: '',
|
||||||
accessor: 'firmware',
|
accessor: 'firmware',
|
||||||
Cell: (v) => firmwareCell(v.cell.row.original),
|
Cell: (v) => firmwareCell(v.cell.row.original),
|
||||||
|
stopPropagation: true,
|
||||||
customWidth: '50px',
|
customWidth: '50px',
|
||||||
disableSortBy: true,
|
disableSortBy: true,
|
||||||
},
|
},
|
||||||
@@ -388,7 +392,7 @@ const DeviceListCard = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p={4}>
|
<CardBody p={4}>
|
||||||
<Box overflowX="auto" w="100%">
|
<Box overflowX="auto" w="100%">
|
||||||
<DataTable
|
<DataTable<DeviceWithStatus>
|
||||||
columns={
|
columns={
|
||||||
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
|
columns.filter(({ id }) => !hiddenColumns.find((hidden) => hidden === id)) as {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -406,7 +410,8 @@ const DeviceListCard = () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setPageInfo={setPageInfo}
|
setPageInfo={setPageInfo}
|
||||||
saveSettingsId="gateway.devices.table"
|
saveSettingsId="gateway.devices.table"
|
||||||
minHeight="600px"
|
onRowClick={(device) => navigate(`devices/${device.serialNumber}`)}
|
||||||
|
isRowClickable={() => true}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
@@ -417,6 +422,7 @@ const DeviceListCard = () => {
|
|||||||
<EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} />
|
<EventQueueModal modalProps={eventQueueProps} serialNumber={serialNumber} />
|
||||||
<ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} />
|
<ConfigureModal modalProps={configureModalProps} serialNumber={serialNumber} />
|
||||||
<TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} />
|
<TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} />
|
||||||
|
<RebootModal modalProps={rebootModalProps} serialNumber={serialNumber} />
|
||||||
{scriptModal.modal}
|
{scriptModal.modal}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
|
|||||||
ml={2}
|
ml={2}
|
||||||
/>
|
/>
|
||||||
{isEditingDescription && (
|
{isEditingDescription && (
|
||||||
|
// @ts-ignore
|
||||||
<SaveButton onClick={onSaveDescription} ml={2} isCompact size="sm" isLoading={updateFirmware.isLoading} />
|
<SaveButton onClick={onSaveDescription} ml={2} isCompact size="sm" isLoading={updateFirmware.isLoading} />
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
@@ -202,48 +203,51 @@ const FirmwareDetailsModal = ({ modalProps, firmware }: Props) => {
|
|||||||
isDisabled={!isEditingDescription}
|
isDisabled={!isEditingDescription}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
|
||||||
<FormLabel>
|
|
||||||
{t('common.notes')}{' '}
|
|
||||||
<Popover trigger="click" placement="auto">
|
|
||||||
{({ onClose }) => (
|
|
||||||
<>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<IconButton
|
|
||||||
aria-label={`${t('crud.add')} ${t('common.note')}`}
|
|
||||||
size="sm"
|
|
||||||
icon={<Plus size={20} />}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}>
|
|
||||||
<PopoverArrow />
|
|
||||||
<PopoverCloseButton alignContent="center" mt={1} />
|
|
||||||
<PopoverHeader display="flex">{t('profile.add_new_note')}</PopoverHeader>
|
|
||||||
<PopoverBody>
|
|
||||||
<Box>
|
|
||||||
<Textarea h="100px" placeholder="Your new note" value={newNote} onChange={onNoteChange} />
|
|
||||||
</Box>
|
|
||||||
<Center mt={2}>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
isDisabled={newNote.length === 0}
|
|
||||||
onClick={onNoteSubmit(onClose)}
|
|
||||||
isLoading={updateFirmware.isLoading}
|
|
||||||
>
|
|
||||||
{t('crud.add')}
|
|
||||||
</Button>
|
|
||||||
</Center>
|
|
||||||
</PopoverBody>
|
|
||||||
</PopoverContent>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
</FormLabel>
|
|
||||||
<Box overflowX="auto" overflowY="auto" maxH="400px">
|
|
||||||
<DataTable columns={columns as Column<object>[]} data={notes} obj={t('common.notes')} minHeight="200px" />
|
|
||||||
</Box>
|
|
||||||
</FormControl>
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
<FormControl mt={2}>
|
||||||
|
<FormLabel>
|
||||||
|
{t('common.notes')}{' '}
|
||||||
|
<Popover trigger="click" placement="auto">
|
||||||
|
{({ onClose }) => (
|
||||||
|
<>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton aria-label={`${t('crud.add')} ${t('common.note')}`} size="sm" icon={<Plus size={20} />} />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent w={breakpoint === 'base' ? 'calc(80vw)' : '500px'}>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton alignContent="center" mt={1} />
|
||||||
|
<PopoverHeader display="flex">{t('profile.add_new_note')}</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
<Box>
|
||||||
|
<Textarea h="100px" placeholder="Your new note" value={newNote} onChange={onNoteChange} />
|
||||||
|
</Box>
|
||||||
|
<Center mt={2}>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
isDisabled={newNote.length === 0}
|
||||||
|
onClick={onNoteSubmit(onClose)}
|
||||||
|
isLoading={updateFirmware.isLoading}
|
||||||
|
>
|
||||||
|
{t('crud.add')}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Popover>
|
||||||
|
</FormLabel>
|
||||||
|
</FormControl>
|
||||||
|
<Box overflowX="auto" overflowY="auto" maxH="400px" mb={4}>
|
||||||
|
<DataTable
|
||||||
|
columns={columns as Column<object>[]}
|
||||||
|
data={notes}
|
||||||
|
obj={t('common.notes')}
|
||||||
|
minHeight="200px"
|
||||||
|
showAllRows
|
||||||
|
hideControls
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
108
src/pages/Firmware/List/UpdateDbButton.tsx
Normal file
108
src/pages/Firmware/List/UpdateDbButton.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
|
AlertIcon,
|
||||||
|
AlertTitle,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
IconButton,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
useDisclosure,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Database } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||||
|
import { Modal } from 'components/Modals/Modal';
|
||||||
|
import { useGetFirmwareDbUpdate, useUpdateFirmwareDb } from 'hooks/Network/Firmware';
|
||||||
|
|
||||||
|
const UpdateDbButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const updateDb = useUpdateFirmwareDb();
|
||||||
|
const getLastUpdate = useGetFirmwareDbUpdate();
|
||||||
|
|
||||||
|
const onUpdateClick = async () => {
|
||||||
|
updateDb.mutate(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
id: `firmware-db-update-success`,
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('firmware.started_db_update'),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
toast({
|
||||||
|
id: `firmware-db-update-error`,
|
||||||
|
title: t('common.error'),
|
||||||
|
description: e?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip label={t('firmware.last_db_update_title')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('firmware.last_db_update_title')}
|
||||||
|
colorScheme="teal"
|
||||||
|
icon={<Database size={20} />}
|
||||||
|
onClick={onOpen}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('firmware.last_db_update_modal')}
|
||||||
|
tags={
|
||||||
|
<Tag colorScheme="blue" size="lg">
|
||||||
|
<TagLabel display="flex">
|
||||||
|
<Text mr={1}>Last Update:</Text>
|
||||||
|
<FormattedDate date={getLastUpdate.data?.lastUpdateTime} />
|
||||||
|
</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Alert status="warning">
|
||||||
|
<AlertIcon />
|
||||||
|
<Box>
|
||||||
|
<AlertTitle>{t('common.warning')}</AlertTitle>
|
||||||
|
<AlertDescription>{t('firmware.db_update_warning')}</AlertDescription>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
<Center my={4}>
|
||||||
|
<Button
|
||||||
|
colorScheme="red"
|
||||||
|
leftIcon={<Database size={20} />}
|
||||||
|
onClick={onUpdateClick}
|
||||||
|
isLoading={updateDb.isLoading}
|
||||||
|
>
|
||||||
|
{t('firmware.start_db_update')}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateDbButton;
|
||||||
@@ -11,7 +11,15 @@ const UriCell = ({ uri }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
<Button onClick={copy.onCopy} size="xs" colorScheme="teal" mr={2}>
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
copy.onCopy();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="teal"
|
||||||
|
mr={2}
|
||||||
|
>
|
||||||
{copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')}
|
{copy.hasCopied ? `${t('common.copied')}!` : t('common.copy')}
|
||||||
</Button>
|
</Button>
|
||||||
<Text my="auto">{uri}</Text>
|
<Text my="auto">{uri}</Text>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { MagnifyingGlass } from 'phosphor-react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import FirmwareDetailsModal from './Modal';
|
import FirmwareDetailsModal from './Modal';
|
||||||
|
import UpdateDbButton from './UpdateDbButton';
|
||||||
import UriCell from './UriCell';
|
import UriCell from './UriCell';
|
||||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
@@ -143,6 +144,7 @@ const FirmwareListTable = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Text>{t('controller.firmware.show_dev_releases')}</Text>
|
<Text>{t('controller.firmware.show_dev_releases')}</Text>
|
||||||
<Switch isChecked={showDevFirmware} onChange={toggle} size="lg" />
|
<Switch isChecked={showDevFirmware} onChange={toggle} size="lg" />
|
||||||
|
<UpdateDbButton />
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
getDeviceTypes.refetch();
|
getDeviceTypes.refetch();
|
||||||
@@ -156,13 +158,14 @@ const FirmwareListTable = () => {
|
|||||||
<CardBody p={4}>
|
<CardBody p={4}>
|
||||||
<Box overflowX="auto" w="100%">
|
<Box overflowX="auto" w="100%">
|
||||||
<LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}>
|
<LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}>
|
||||||
<DataTable
|
<DataTable<Firmware>
|
||||||
columns={columns as Column<object>[]}
|
columns={columns}
|
||||||
saveSettingsId="firmware.table"
|
saveSettingsId="firmware.table"
|
||||||
data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []}
|
data={getFirmware.data?.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel')) ?? []}
|
||||||
obj={t('analytics.firmware')}
|
obj={t('analytics.firmware')}
|
||||||
minHeight="200px"
|
minHeight="200px"
|
||||||
sortBy={[{ id: 'imageDate', desc: true }]}
|
sortBy={[{ id: 'imageDate', desc: true }]}
|
||||||
|
onRowClick={(firmw) => handleViewDetailsClick(firmw)()}
|
||||||
/>
|
/>
|
||||||
</LoadingOverlay>
|
</LoadingOverlay>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ const _LoginForm: React.FC<_LoginFormProps> = ({ setActiveForm }) => {
|
|||||||
const displayError = useMemo(() => {
|
const displayError = useMemo(() => {
|
||||||
const loginError: AxiosError = error as AxiosError;
|
const loginError: AxiosError = error as AxiosError;
|
||||||
|
|
||||||
if (loginError?.response?.data?.ErrorCode === 4) return t('login.waiting_for_email_verification');
|
if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification');
|
||||||
|
if (loginError?.response?.data?.ErrorCode === 15) {
|
||||||
|
return t('login.suspended_error');
|
||||||
|
}
|
||||||
return t('login.invalid_credentials');
|
return t('login.invalid_credentials');
|
||||||
}, [t, error]);
|
}, [t, error]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
import { Box, Flex, HStack, IconButton, Select, Spacer, Table, Text, Th, Thead, Tooltip, Tr } from '@chakra-ui/react';
|
||||||
import { Download } from 'phosphor-react';
|
import { Download } from 'phosphor-react';
|
||||||
import { CSVLink } from 'react-csv';
|
import { CSVLink } from 'react-csv';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -127,9 +127,9 @@ const LogsCard = () => {
|
|||||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
data={downloadableLogs as object[]}
|
data={downloadableLogs as object[]}
|
||||||
>
|
>
|
||||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
<Tooltip label={t('logs.export')} hasArrow>
|
||||||
{t('logs.export')}
|
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||||
</Button>
|
</Tooltip>
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
Spacer,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tooltip,
|
||||||
|
Tr,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { Download } from 'phosphor-react';
|
import { Download } from 'phosphor-react';
|
||||||
import { CSVLink } from 'react-csv';
|
import { CSVLink } from 'react-csv';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -128,9 +142,9 @@ const FmsLogsCard = () => {
|
|||||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
data={downloadableLogs as object[]}
|
data={downloadableLogs as object[]}
|
||||||
>
|
>
|
||||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
<Tooltip label={t('logs.export')} hasArrow>
|
||||||
{t('logs.export')}
|
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||||
</Button>
|
</Tooltip>
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
Spacer,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tooltip,
|
||||||
|
Tr,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { Download } from 'phosphor-react';
|
import { Download } from 'phosphor-react';
|
||||||
import { CSVLink } from 'react-csv';
|
import { CSVLink } from 'react-csv';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -128,9 +142,9 @@ const GeneralLogsCard = () => {
|
|||||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
data={downloadableLogs as object[]}
|
data={downloadableLogs as object[]}
|
||||||
>
|
>
|
||||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
<Tooltip label={t('logs.export')} hasArrow>
|
||||||
{t('logs.export')}
|
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||||
</Button>
|
</Tooltip>
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
IconButton,
|
||||||
|
Select,
|
||||||
|
Spacer,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tooltip,
|
||||||
|
Tr,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import { Download } from 'phosphor-react';
|
import { Download } from 'phosphor-react';
|
||||||
import { CSVLink } from 'react-csv';
|
import { CSVLink } from 'react-csv';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -128,9 +142,9 @@ const SecLogsCard = () => {
|
|||||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
data={downloadableLogs as object[]}
|
data={downloadableLogs as object[]}
|
||||||
>
|
>
|
||||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
<Tooltip label={t('logs.export')} hasArrow>
|
||||||
{t('logs.export')}
|
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||||
</Button>
|
</Tooltip>
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
import ApiKeyTable from './Table';
|
import ApiKeyTable from './Table';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
@@ -10,7 +11,9 @@ const ApiKeysCard = () => {
|
|||||||
return (
|
return (
|
||||||
<Card p={4}>
|
<Card p={4}>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<ApiKeyTable userId={user?.id ?? ''} />
|
<Box w="100%">
|
||||||
|
<ApiKeyTable userId={user?.id ?? ''} />
|
||||||
|
</Box>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react';
|
import { Box, Button, Heading, HStack, Spacer } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import ScriptTableActions from './Actions';
|
import ScriptTableActions from './Actions';
|
||||||
import CreateScriptButton from './CreateButton';
|
import CreateScriptButton from './CreateButton';
|
||||||
import useScriptsTable from './useScriptsTable';
|
import useScriptsTable from './useScriptsTable';
|
||||||
@@ -21,6 +22,7 @@ type Props = {
|
|||||||
const ScriptTableCard = ({ onIdSelect }: Props) => {
|
const ScriptTableCard = ({ onIdSelect }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { query, hiddenColumns } = useScriptsTable();
|
const { query, hiddenColumns } = useScriptsTable();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []);
|
const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []);
|
||||||
const actionCell = React.useCallback(
|
const actionCell = React.useCallback(
|
||||||
@@ -108,8 +110,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Box w="100%" h="300px" overflowY="auto">
|
<Box w="100%" h="300px" overflowY="auto">
|
||||||
<DataTable
|
<DataTable<Script>
|
||||||
columns={columns as Column<object>[]}
|
columns={columns}
|
||||||
saveSettingsId="apiKeys.profile.table"
|
saveSettingsId="apiKeys.profile.table"
|
||||||
data={query.data ?? []}
|
data={query.data ?? []}
|
||||||
obj={t('script.other')}
|
obj={t('script.other')}
|
||||||
@@ -118,6 +120,8 @@ const ScriptTableCard = ({ onIdSelect }: Props) => {
|
|||||||
hiddenColumns={hiddenColumns[0]}
|
hiddenColumns={hiddenColumns[0]}
|
||||||
showAllRows
|
showAllRows
|
||||||
hideControls
|
hideControls
|
||||||
|
onRowClick={(script) => onIdSelect(script.id)}
|
||||||
|
isRowClickable={(script) => script.id !== id}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
139
src/pages/SystemPage/SystemSecrets/Actions.tsx
Normal file
139
src/pages/SystemPage/SystemSecrets/Actions.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CopyIcon } from '@chakra-ui/icons';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverFooter,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTrigger,
|
||||||
|
Center,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
useClipboard,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Eye, Trash } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import EditSecretButton from './EditButton';
|
||||||
|
import { Secret, useDeleteSystemSecret } from 'hooks/Network/Secrets';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
secret: Secret;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SystemSecretActions = ({ secret, isDisabled }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const deleteSecret = useDeleteSystemSecret();
|
||||||
|
const { hasCopied, onCopy } = useClipboard(secret.value);
|
||||||
|
|
||||||
|
const handleDeleteClick = React.useCallback(() => {
|
||||||
|
deleteSecret.mutate(secret.key, {
|
||||||
|
onSuccess: () => {
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack mx="auto">
|
||||||
|
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||||
|
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||||
|
<Box>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton
|
||||||
|
aria-label="delete-device"
|
||||||
|
colorScheme="red"
|
||||||
|
icon={<Trash size={20} />}
|
||||||
|
size="sm"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent w="340px">
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton />
|
||||||
|
<PopoverHeader>
|
||||||
|
{t('crud.delete')} {secret.key}
|
||||||
|
</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
<Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('system.secrets_one') })}</Text>
|
||||||
|
</PopoverBody>
|
||||||
|
<PopoverFooter>
|
||||||
|
<Center>
|
||||||
|
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteSecret.isLoading}>
|
||||||
|
{t('common.yes')}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</PopoverFooter>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Tooltip
|
||||||
|
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`}
|
||||||
|
hasArrow
|
||||||
|
closeOnClick={false}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('common.copy')}
|
||||||
|
icon={<CopyIcon h={5} w={5} />}
|
||||||
|
onClick={onCopy}
|
||||||
|
size="sm"
|
||||||
|
colorScheme="teal"
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<EditSecretButton secret={secret} />
|
||||||
|
<Popover>
|
||||||
|
<Tooltip label={`${t('common.view')} ${t('system.secrets_one')}`} hasArrow closeOnClick={false}>
|
||||||
|
<Box>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent w="560px">
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton />
|
||||||
|
<PopoverHeader>
|
||||||
|
{t('common.view')} {secret.key}
|
||||||
|
<Tooltip
|
||||||
|
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`}
|
||||||
|
hasArrow
|
||||||
|
closeOnClick={false}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('common.copy')}
|
||||||
|
icon={<CopyIcon h={4} w={4} />}
|
||||||
|
onClick={onCopy}
|
||||||
|
size="xs"
|
||||||
|
colorScheme="teal"
|
||||||
|
ml={2}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
<Text whiteSpace="break-spaces">
|
||||||
|
<Center>
|
||||||
|
<pre style={{ fontFamily: 'monospace' }}>{secret.value}</pre>
|
||||||
|
</Center>
|
||||||
|
</Text>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemSecretActions;
|
||||||
118
src/pages/SystemPage/SystemSecrets/CreateButton.tsx
Normal file
118
src/pages/SystemPage/SystemSecrets/CreateButton.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
FormErrorMessage,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
useDisclosure,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CreateButton } from '../../../components/Buttons/CreateButton';
|
||||||
|
import { SaveButton } from '../../../components/Buttons/SaveButton';
|
||||||
|
import { Modal } from '../../../components/Modals/Modal';
|
||||||
|
import { useCreateSystemSecret } from 'hooks/Network/Secrets';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FORM_VALUES: FormValues = {
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemSecretCreateButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [form, setForm] = React.useState<FormValues>(DEFAULT_FORM_VALUES);
|
||||||
|
const [isNameChanged, setIsNameChanged] = React.useState(false);
|
||||||
|
const [isValueChanged, setIsValueChanged] = React.useState(false);
|
||||||
|
const create = useCreateSystemSecret();
|
||||||
|
|
||||||
|
const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setForm({ ...form, key: e.target.value });
|
||||||
|
if (!isNameChanged) setIsNameChanged(true);
|
||||||
|
};
|
||||||
|
const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setForm({ ...form, value: e.target.value });
|
||||||
|
if (!isValueChanged) setIsValueChanged(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNameError = form.key.length === 0;
|
||||||
|
const isValueError = form.value.length === 0;
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
create.mutate(form, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
id: 'create-system-secret-success',
|
||||||
|
title: t('common.success'),
|
||||||
|
description: t('crud.success_update_obj', {
|
||||||
|
obj: t('system.secrets_one'),
|
||||||
|
}),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
id: 'create-system-secret-error',
|
||||||
|
title: t('common.error'),
|
||||||
|
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenClick = () => {
|
||||||
|
setIsNameChanged(false);
|
||||||
|
setIsValueChanged(false);
|
||||||
|
setForm(DEFAULT_FORM_VALUES);
|
||||||
|
onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateButton onClick={handleOpenClick} isCompact />
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title={t('system.secrets_create')}
|
||||||
|
topRightButtons={
|
||||||
|
<SaveButton onClick={onSubmit} isDisabled={isNameError || isValueError} isLoading={create.isLoading} />
|
||||||
|
}
|
||||||
|
options={{
|
||||||
|
modalSize: 'sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<FormControl mb={2} isInvalid={isNameError && isNameChanged}>
|
||||||
|
<FormLabel>{t('common.name')}</FormLabel>
|
||||||
|
<Input value={form.key} onChange={onKeyChange} />
|
||||||
|
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mb={2} isInvalid={isValueError && isValueChanged}>
|
||||||
|
<FormLabel>{t('common.value')}</FormLabel>
|
||||||
|
<Textarea value={form.value} onChange={onValueChange} rows={2} />
|
||||||
|
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemSecretCreateButton;
|
||||||
146
src/pages/SystemPage/SystemSecrets/EditButton.tsx
Normal file
146
src/pages/SystemPage/SystemSecrets/EditButton.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
FormControl,
|
||||||
|
FormErrorMessage,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverFooter,
|
||||||
|
PopoverHeader,
|
||||||
|
PopoverTrigger,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
Tooltip,
|
||||||
|
useDisclosure,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Pencil } from 'phosphor-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Secret, useUpdateSystemSecret } from 'hooks/Network/Secrets';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: Secret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditSecretButton = ({ secret }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [form, setForm] = React.useState<FormValues>({
|
||||||
|
key: secret.key,
|
||||||
|
value: secret.value,
|
||||||
|
});
|
||||||
|
const [isNameChanged, setIsNameChanged] = React.useState(false);
|
||||||
|
const [isValueChanged, setIsValueChanged] = React.useState(false);
|
||||||
|
const update = useUpdateSystemSecret();
|
||||||
|
|
||||||
|
const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setForm({ ...form, key: e.target.value });
|
||||||
|
if (!isNameChanged) setIsNameChanged(true);
|
||||||
|
};
|
||||||
|
const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setForm({ ...form, value: e.target.value });
|
||||||
|
if (!isValueChanged) setIsValueChanged(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNameError = form.key.length === 0;
|
||||||
|
const isValueError = form.value.length === 0;
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
update.mutate(form, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
id: 'create-system-secret-success',
|
||||||
|
title: t('common.success'),
|
||||||
|
status: 'success',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
toast({
|
||||||
|
id: 'create-system-secret-error',
|
||||||
|
title: t('common.error'),
|
||||||
|
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenClick = () => {
|
||||||
|
setIsNameChanged(false);
|
||||||
|
setIsValueChanged(false);
|
||||||
|
setForm({
|
||||||
|
key: secret.key,
|
||||||
|
value: secret.value,
|
||||||
|
});
|
||||||
|
onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover isOpen={isOpen} onOpen={handleOpenClick} onClose={onClose}>
|
||||||
|
<Tooltip hasArrow label={t('crud.edit')} placement="top" isDisabled={isOpen}>
|
||||||
|
<Box>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<IconButton aria-label="delete-device" colorScheme="blue" icon={<Pencil size={20} />} size="sm" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent w="340px">
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton />
|
||||||
|
<PopoverHeader>
|
||||||
|
{t('crud.edit')} {secret.key}
|
||||||
|
</PopoverHeader>
|
||||||
|
<PopoverBody>
|
||||||
|
<Text whiteSpace="break-spaces">
|
||||||
|
<Box>
|
||||||
|
<FormControl mb={2} isInvalid={isNameError && isNameChanged}>
|
||||||
|
<FormLabel>{t('common.name')}</FormLabel>
|
||||||
|
<Input value={form.key} onChange={onKeyChange} />
|
||||||
|
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl mb={2} isInvalid={isValueError && isValueChanged}>
|
||||||
|
<FormLabel>{t('common.value')}</FormLabel>
|
||||||
|
<Textarea value={form.value} onChange={onValueChange} rows={2} />
|
||||||
|
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Text>
|
||||||
|
</PopoverBody>
|
||||||
|
<PopoverFooter>
|
||||||
|
<Center>
|
||||||
|
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="blue" ml="1" onClick={onSubmit} isLoading={update.isLoading}>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Center>
|
||||||
|
</PopoverFooter>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditSecretButton;
|
||||||
70
src/pages/SystemPage/SystemSecrets/Table.tsx
Normal file
70
src/pages/SystemPage/SystemSecrets/Table.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DataTable } from '../../../components/DataTables/DataTable';
|
||||||
|
import SystemSecretActions from './Actions';
|
||||||
|
import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets';
|
||||||
|
import { Column } from 'models/Table';
|
||||||
|
|
||||||
|
const SystemSecretsTable = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const getSecrets = useGetAllSystemSecrets();
|
||||||
|
const getDictionary = useGetSystemSecretsDictionary();
|
||||||
|
|
||||||
|
const descriptionCell = React.useCallback(
|
||||||
|
(secret: Secret) => {
|
||||||
|
if (!getDictionary.data) return '-';
|
||||||
|
|
||||||
|
return getDictionary.data.find((d) => d.key === secret.key)?.description ?? '-';
|
||||||
|
},
|
||||||
|
[getDictionary.data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionCell = React.useCallback((secret: Secret) => <SystemSecretActions secret={secret} />, []);
|
||||||
|
|
||||||
|
const columns = React.useMemo(
|
||||||
|
(): Column<Secret>[] => [
|
||||||
|
{
|
||||||
|
id: 'key',
|
||||||
|
Header: t('common.name'),
|
||||||
|
Footer: '',
|
||||||
|
accessor: 'key',
|
||||||
|
alwaysShow: true,
|
||||||
|
customWidth: '120px',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'description',
|
||||||
|
Header: t('common.description'),
|
||||||
|
Footer: '',
|
||||||
|
Cell: ({ cell }) => descriptionCell(cell.row.original),
|
||||||
|
accessor: 'description',
|
||||||
|
hasPopover: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
Header: t('common.actions'),
|
||||||
|
Footer: '',
|
||||||
|
Cell: (v) => actionCell(v.cell.row.original),
|
||||||
|
disableSortBy: true,
|
||||||
|
customWidth: '120px',
|
||||||
|
alwaysShow: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[t, descriptionCell],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Box w="100%">
|
||||||
|
<DataTable
|
||||||
|
columns={columns as Column<object>[]}
|
||||||
|
saveSettingsId="system.secrets.table"
|
||||||
|
data={getSecrets.data ?? []}
|
||||||
|
obj={t('keys.other')}
|
||||||
|
sortBy={[{ id: 'key', desc: false }]}
|
||||||
|
showAllRows
|
||||||
|
hideControls
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemSecretsTable;
|
||||||
53
src/pages/SystemPage/SystemSecrets/index.tsx
Normal file
53
src/pages/SystemPage/SystemSecrets/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
BackgroundProps,
|
||||||
|
Box,
|
||||||
|
EffectProps,
|
||||||
|
Heading,
|
||||||
|
InteractivityProps,
|
||||||
|
LayoutProps,
|
||||||
|
PositionProps,
|
||||||
|
SpaceProps,
|
||||||
|
Spacer,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import SystemSecretCreateButton from './CreateButton';
|
||||||
|
import SystemSecretsTable from './Table';
|
||||||
|
import { Card } from 'components/Containers/Card';
|
||||||
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
|
export interface SystemSecretsCardProps
|
||||||
|
extends LayoutProps,
|
||||||
|
SpaceProps,
|
||||||
|
BackgroundProps,
|
||||||
|
InteractivityProps,
|
||||||
|
PositionProps,
|
||||||
|
EffectProps {}
|
||||||
|
|
||||||
|
export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
if (!user || user.userRole !== 'root') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={4} py={4}>
|
||||||
|
<Card variant="widget" {...props}>
|
||||||
|
<CardHeader>
|
||||||
|
<Heading size="md" my="auto">
|
||||||
|
{t('system.secrets')}
|
||||||
|
</Heading>
|
||||||
|
<Spacer />
|
||||||
|
<SystemSecretCreateButton />
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<SystemSecretsTable />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,8 +21,8 @@ import axios from 'axios';
|
|||||||
import { FloppyDisk } from 'phosphor-react';
|
import { FloppyDisk } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { Modal } from '../../../../components/Modals/Modal';
|
||||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
import { Modal } from 'components/Modals/Modal';
|
|
||||||
import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System';
|
import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { Article } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SystemLoggingModal from './Modal';
|
import SystemLoggingModal from './Modal';
|
||||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||||
@@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto">
|
<Tooltip label={t('system.logging')} hasArrow>
|
||||||
{t('system.logging')}
|
<IconButton
|
||||||
</Button>
|
aria-label={t('system.logging')}
|
||||||
|
colorScheme="teal"
|
||||||
|
type="button"
|
||||||
|
my="auto"
|
||||||
|
onClick={modalProps.onOpen}
|
||||||
|
icon={<Article size={20} />}
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
|
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DataTable } from 'components/DataTables/DataTable';
|
import { DataTable } from '../../../components/DataTables/DataTable';
|
||||||
import { compactDate } from 'helpers/dateFormatting';
|
import { compactDate } from 'helpers/dateFormatting';
|
||||||
import { Column } from 'models/Table';
|
import { Column } from 'models/Table';
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ import {
|
|||||||
import { MultiValue, Select } from 'chakra-react-select';
|
import { MultiValue, Select } from 'chakra-react-select';
|
||||||
import { ArrowsClockwise } from 'phosphor-react';
|
import { ArrowsClockwise } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
|
||||||
import SystemLoggingButton from './LoggingButton';
|
import SystemLoggingButton from './LoggingButton';
|
||||||
import SystemCertificatesTable from './SystemCertificatesTable';
|
import SystemCertificatesTable from './SystemCertificatesTable';
|
||||||
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
|
||||||
import { compactSecondsToDetailed } from 'helpers/dateFormatting';
|
import { compactSecondsToDetailed } from 'helpers/dateFormatting';
|
||||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||||
import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System';
|
import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System';
|
||||||
@@ -65,21 +66,12 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card variant="widget">
|
||||||
<Box display="flex" mb={2}>
|
<Box display="flex" mb={2}>
|
||||||
<Heading pt={0}>{endpoint.type}</Heading>
|
<Heading pt={0}>{endpoint.type}</Heading>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<SystemLoggingButton endpoint={endpoint} token={token} />
|
<SystemLoggingButton endpoint={endpoint} token={token} />
|
||||||
<Button
|
<RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
|
||||||
mt={1}
|
|
||||||
minWidth="112px"
|
|
||||||
colorScheme="gray"
|
|
||||||
rightIcon={<ArrowsClockwise />}
|
|
||||||
onClick={refresh}
|
|
||||||
isLoading={isFetchingSystem || isFetchingSubsystems}
|
|
||||||
>
|
|
||||||
{t('common.refresh')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<VStack w="100%">
|
<VStack w="100%">
|
||||||
@@ -179,7 +171,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
|||||||
ml={2}
|
ml={2}
|
||||||
onClick={handleReloadClick}
|
onClick={handleReloadClick}
|
||||||
icon={<ArrowsClockwise size={20} />}
|
icon={<ArrowsClockwise size={20} />}
|
||||||
colorScheme="gray"
|
colorScheme="blue"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
isDisabled={subs.length === 0}
|
isDisabled={subs.length === 0}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,27 +1,47 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { RefreshButton } from '../../components/Buttons/RefreshButton';
|
||||||
|
import { SystemSecretsCard } from './SystemSecrets';
|
||||||
import SystemTile from './SystemTile';
|
import SystemTile from './SystemTile';
|
||||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { useGetEndpoints } from 'hooks/Network/Endpoints';
|
import { useGetEndpoints } from 'hooks/Network/Endpoints';
|
||||||
|
|
||||||
const SystemPage = () => {
|
const getDefaultTabIndex = () => {
|
||||||
|
const index = localStorage.getItem('system-tab-index') || '0';
|
||||||
|
try {
|
||||||
|
return parseInt(index, 10);
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
type Props = {
|
||||||
|
isOnlySec?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SystemPage = ({ isOnlySec }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { token } = useAuth();
|
const { token, user } = useAuth();
|
||||||
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
|
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
|
||||||
|
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||||
|
const handleTabChange = (index: number) => {
|
||||||
|
setTabIndex(index);
|
||||||
|
localStorage.setItem('system-tab-index', index.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRoot = user && user.userRole === 'root';
|
||||||
|
|
||||||
const endpointsList = React.useMemo(() => {
|
const endpointsList = React.useMemo(() => {
|
||||||
if (!endpoints || !token) return null;
|
if (!token || (!isOnlySec && !endpoints)) return null;
|
||||||
|
|
||||||
const endpointList = [...endpoints];
|
const endpointList = endpoints ? [...endpoints] : [];
|
||||||
endpointList.push({
|
endpointList.push({
|
||||||
uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '',
|
uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '',
|
||||||
type: 'owsec',
|
type: isOnlySec ? '' : 'owsec',
|
||||||
id: 0,
|
id: 0,
|
||||||
vendor: 'owsec',
|
vendor: 'owsec',
|
||||||
authenticationType: '',
|
authenticationType: '',
|
||||||
@@ -37,20 +57,51 @@ const SystemPage = () => {
|
|||||||
}, [endpoints, token]);
|
}, [endpoints, token]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Card p={0}>
|
||||||
<Card mb={4} py={2} px={4}>
|
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||||
<CardHeader>
|
<TabList>
|
||||||
<Heading size="md" my="auto">
|
<CardHeader>
|
||||||
{t('controller.firmware.endpoints')}
|
<Tab>{t('system.services')}</Tab>
|
||||||
</Heading>
|
<Tab hidden={!isRoot}>{t('system.configuration')}</Tab>
|
||||||
<Spacer />
|
</CardHeader>
|
||||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
</TabList>
|
||||||
</CardHeader>
|
<TabPanels>
|
||||||
</Card>
|
<TabPanel p={0}>
|
||||||
<SimpleGrid minChildWidth="500px" spacing="20px" mb={3}>
|
<Box
|
||||||
{endpointsList}
|
borderLeft="1px solid"
|
||||||
</SimpleGrid>
|
borderRight="1px solid"
|
||||||
</>
|
borderBottom="1px solid"
|
||||||
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
|
borderBottomLeftRadius="15px"
|
||||||
|
borderBottomRightRadius="15px"
|
||||||
|
>
|
||||||
|
{!isOnlySec && (
|
||||||
|
<CardHeader px={4} pt={4}>
|
||||||
|
<Spacer />
|
||||||
|
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||||
|
</CardHeader>
|
||||||
|
)}
|
||||||
|
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
|
||||||
|
{endpointsList}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel p={0}>
|
||||||
|
<Box
|
||||||
|
borderLeft="1px solid"
|
||||||
|
borderRight="1px solid"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||||
|
borderBottomLeftRadius="15px"
|
||||||
|
borderBottomRightRadius="15px"
|
||||||
|
>
|
||||||
|
<SystemSecretsCard />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SystemPage;
|
export default SystemPage;
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ interface Props {
|
|||||||
isSuspended: boolean;
|
isSuspended: boolean;
|
||||||
isWaitingForCheck: boolean;
|
isWaitingForCheck: boolean;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refresh }) => {
|
const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh });
|
const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh });
|
||||||
@@ -75,10 +77,17 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Tooltip label={t('commands.other')}>
|
<Tooltip label={t('common.actions')}>
|
||||||
<MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} />
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
aria-label="Commands"
|
||||||
|
icon={<Wrench size={20} />}
|
||||||
|
size={size}
|
||||||
|
ml={2}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<MenuList>
|
<MenuList fontSize="md">
|
||||||
<MenuItem onClick={handleSuspendClick}>
|
<MenuItem onClick={handleSuspendClick}>
|
||||||
{isSuspended ? t('users.reactivate_user') : t('users.suspend')}
|
{isSuspended ? t('users.reactivate_user') : t('users.suspend')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { AddIcon } from '@chakra-ui/icons';
|
import { useDisclosure } from '@chakra-ui/react';
|
||||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||||
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||||
import { Modal } from '../../../../components/Modals/Modal';
|
import { Modal } from '../../../../components/Modals/Modal';
|
||||||
import CreateUserForm, { CreateUserFormValues } from './Form';
|
import CreateUserForm, { CreateUserFormValues } from './Form';
|
||||||
|
import { CreateButton } from 'components/Buttons/CreateButton';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { useFormRef } from 'hooks/useFormRef';
|
import { useFormRef } from 'hooks/useFormRef';
|
||||||
|
|
||||||
@@ -25,16 +25,7 @@ const CreateUserModal = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||||
hidden={user?.userRole === 'csr'}
|
|
||||||
alignItems="center"
|
|
||||||
colorScheme="blue"
|
|
||||||
rightIcon={<AddIcon />}
|
|
||||||
onClick={onOpen}
|
|
||||||
ml={2}
|
|
||||||
>
|
|
||||||
{t('crud.create')}
|
|
||||||
</Button>
|
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Pro
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormKey(uuid());
|
setFormKey(uuid());
|
||||||
}, [isOpen]);
|
}, [isOpen, editing]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Spinner, Center, useDisclosure, useBoolean } from '@chakra-ui/react';
|
import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EditButton } from '../../../../components/Buttons/EditButton';
|
|
||||||
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||||
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||||
import { Modal } from '../../../../components/Modals/Modal';
|
import { Modal } from '../../../../components/Modals/Modal';
|
||||||
|
import ActionsDropdown from '../ActionsDropdown';
|
||||||
import UpdateUserForm from './Form';
|
import UpdateUserForm from './Form';
|
||||||
|
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
||||||
import { useGetUser, User } from 'hooks/Network/Users';
|
import { useGetUser, User } from 'hooks/Network/Users';
|
||||||
import { useFormRef } from 'hooks/useFormRef';
|
import { useFormRef } from 'hooks/useFormRef';
|
||||||
|
|
||||||
@@ -19,10 +21,11 @@ type Props = {
|
|||||||
const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [editing, setEditing] = useBoolean();
|
const [editing, setEditing] = useBoolean();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||||
const { form, formRef } = useFormRef<User>();
|
const { form, formRef } = useFormRef<User>();
|
||||||
const canFetchUser = userId !== '' && isOpen;
|
const canFetchUser = userId !== '' && isOpen;
|
||||||
const { data: user, isFetching } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
||||||
|
|
||||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||||
|
|
||||||
@@ -31,6 +34,11 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refresh = () => {
|
||||||
|
refetch();
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) setEditing.off();
|
if (isOpen) setEditing.off();
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@@ -40,15 +48,40 @@ const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={closeModal}
|
onClose={closeModal}
|
||||||
title={t('crud.edit_obj', { obj: t('user.title') })}
|
title={user?.name ?? t('crud.edit_obj', { obj: t('user.title') })}
|
||||||
|
tags={
|
||||||
|
<>
|
||||||
|
{user?.suspended ? (
|
||||||
|
<Tag colorScheme="yellow" size="lg">
|
||||||
|
{t('user.suspended')}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
{user?.waitingForEmailCheck ? (
|
||||||
|
<Tag colorScheme="blue" size="lg">
|
||||||
|
{t('user.email_not_validated')}
|
||||||
|
</Tag>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
}
|
||||||
topRightButtons={
|
topRightButtons={
|
||||||
<>
|
<>
|
||||||
<SaveButton
|
<SaveButton
|
||||||
onClick={form.submitForm}
|
onClick={form.submitForm}
|
||||||
isLoading={form.isSubmitting}
|
isLoading={form.isSubmitting}
|
||||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||||
|
hidden={!editing}
|
||||||
/>
|
/>
|
||||||
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
|
<ToggleEditButton ml={2} isEditing={editing} toggleEdit={setEditing.toggle} isDirty={form.dirty} />
|
||||||
|
{user ? (
|
||||||
|
<ActionsDropdown
|
||||||
|
id={user?.id}
|
||||||
|
isSuspended={user?.suspended}
|
||||||
|
isWaitingForCheck={user?.waitingForEmailCheck}
|
||||||
|
refresh={refresh}
|
||||||
|
size="md"
|
||||||
|
isDisabled={editing}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Avatar, Box, Button, Flex, useDisclosure } from '@chakra-ui/react';
|
import { Avatar, Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||||
import { ArrowsClockwise } from 'phosphor-react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
|
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
|
||||||
@@ -9,6 +8,7 @@ import FormattedDate from '../../../components/InformationDisplays/FormattedDate
|
|||||||
import CreateUserModal from './CreateUserModal';
|
import CreateUserModal from './CreateUserModal';
|
||||||
import EditUserModal from './EditUserModal';
|
import EditUserModal from './EditUserModal';
|
||||||
import UserActions from './UserActions';
|
import UserActions from './UserActions';
|
||||||
|
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
@@ -25,10 +25,10 @@ const UserTable = () => {
|
|||||||
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||||
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
|
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
|
||||||
|
|
||||||
const openEditModal = (editUser: User) => {
|
const openEditModal = React.useCallback((editUser: User) => {
|
||||||
setEditId(editUser.id);
|
setEditId(editUser.id);
|
||||||
openEdit();
|
openEdit();
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const memoizedActions = useCallback(
|
const memoizedActions = useCallback(
|
||||||
(userActions: User) => (
|
(userActions: User) => (
|
||||||
@@ -99,7 +99,7 @@ const UserTable = () => {
|
|||||||
];
|
];
|
||||||
if (user?.userRole !== 'csr')
|
if (user?.userRole !== 'csr')
|
||||||
baseColumns.push({
|
baseColumns.push({
|
||||||
id: 'user',
|
id: 'actions',
|
||||||
Header: t('common.actions'),
|
Header: t('common.actions'),
|
||||||
Footer: '',
|
Footer: '',
|
||||||
accessor: 'Id',
|
accessor: 'Id',
|
||||||
@@ -125,28 +125,21 @@ const UserTable = () => {
|
|||||||
preference="provisioning.userTable.hiddenColumns"
|
preference="provisioning.userTable.hiddenColumns"
|
||||||
/>
|
/>
|
||||||
<CreateUserModal />
|
<CreateUserModal />
|
||||||
<Button
|
<RefreshButton onClick={refreshUsers} isFetching={isFetching} ml={2} />
|
||||||
colorScheme="gray"
|
|
||||||
onClick={() => refreshUsers()}
|
|
||||||
rightIcon={<ArrowsClockwise />}
|
|
||||||
ml={2}
|
|
||||||
isLoading={isFetching}
|
|
||||||
>
|
|
||||||
{t('common.refresh')}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Box overflowX="auto" w="100%">
|
<Box overflowX="auto" w="100%">
|
||||||
<DataTable
|
<DataTable<User>
|
||||||
columns={columns as Column<object>[]}
|
columns={columns}
|
||||||
data={users?.filter((curr) => curr.email !== user?.email) ?? []}
|
data={users ?? []}
|
||||||
isLoading={isFetching}
|
isLoading={isFetching}
|
||||||
obj={t('users.title')}
|
obj={t('users.title')}
|
||||||
sortBy={[{ id: 'email', desc: false }]}
|
sortBy={[{ id: 'email', desc: false }]}
|
||||||
hiddenColumns={hiddenColumns}
|
hiddenColumns={hiddenColumns}
|
||||||
fullScreen
|
fullScreen
|
||||||
|
onRowClick={openEditModal}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
|
import { extendTheme, Tooltip, type ThemeConfig } from '@chakra-ui/react';
|
||||||
import CardComponent from './additions/card/Card';
|
import CardComponent from './additions/card/Card';
|
||||||
import CardBodyComponent from './additions/card/CardBody';
|
import CardBodyComponent from './additions/card/CardBody';
|
||||||
import CardHeaderComponent from './additions/card/CardHeader';
|
import CardHeaderComponent from './additions/card/CardHeader';
|
||||||
@@ -37,4 +37,6 @@ const theme = extendTheme({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Tooltip.defaultProps = { ...Tooltip.defaultProps, hasArrow: true };
|
||||||
|
|
||||||
export default theme;
|
export default theme;
|
||||||
|
|||||||
@@ -1,49 +1,9 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [tsconfigPaths(), react()],
|
||||||
tsconfigPaths(),
|
|
||||||
react(),
|
|
||||||
VitePWA({
|
|
||||||
registerType: 'autoUpdate',
|
|
||||||
devOptions: {
|
|
||||||
enabled: true,
|
|
||||||
/* other options */
|
|
||||||
},
|
|
||||||
manifest: {
|
|
||||||
name: 'OpenWiFi Controller App',
|
|
||||||
short_name: 'OpenWiFiController',
|
|
||||||
description: 'OpenWiFi Controller App',
|
|
||||||
theme_color: '#000000',
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: 'android-chrome-192x192.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'android-chrome-384x384.png',
|
|
||||||
sizes: '384x384',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'android-chrome-512x512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'android-chrome-512x512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'any maskable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
build: {
|
build: {
|
||||||
outDir: './build',
|
outDir: './build',
|
||||||
chunkSizeWarningLimit: 1000,
|
chunkSizeWarningLimit: 1000,
|
||||||
|
|||||||
Reference in New Issue
Block a user