Compare commits
43 commits
plenum_amb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
696fbb3593 | ||
|
|
ddd0b5062b | ||
|
|
80750bd3e9 | ||
|
|
65f8fd06d3 | ||
|
|
199511e829 | ||
|
|
b272d264b6 | ||
|
|
cb168aef9b | ||
|
|
d124db62e0 | ||
|
|
de662907c6 | ||
|
|
487afc6464 | ||
|
|
fc347938d2 | ||
|
|
72c920404c | ||
|
|
3fc663a4b6 | ||
|
|
2c17fb655c | ||
|
|
71fb67bba4 | ||
|
|
10f0aa46cc | ||
|
|
8706446108 | ||
|
|
9534eed94c | ||
|
|
3eb66e3622 | ||
|
|
5dc3a3e975 | ||
|
|
7666fbf9b6 | ||
|
|
64fcc934ce | ||
|
|
cca981308d | ||
|
|
ffd5e0a769 | ||
|
|
e8cc509915 | ||
|
|
b9a13c5781 | ||
|
|
879a5ae1f0 | ||
|
|
895ed8e50b | ||
|
|
4ea0d53c94 | ||
|
|
6b07859d00 | ||
|
|
f73eb0fcbd | ||
|
|
728d07a07e | ||
|
|
e19e1f03eb | ||
|
|
270df1a118 | ||
|
|
939a53d072 | ||
|
|
0984ae0f37 | ||
|
|
d9846ea02b | ||
|
|
b378764d62 | ||
|
|
c4999a0ba5 | ||
|
|
1051534bb5 | ||
|
|
bbc9c4ee7f | ||
|
|
c0a3e23d67 | ||
|
|
90ad5f8b12 |
36 changed files with 1312 additions and 422 deletions
412
package-lock.json
generated
412
package-lock.json
generated
|
|
@ -46,7 +46,7 @@
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"openweathermap-ts": "^1.2.10",
|
"openweathermap-ts": "^1.2.10",
|
||||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#sendgrid_whitelabel",
|
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
|
||||||
"query-string": "^9.2.1",
|
"query-string": "^9.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
@ -146,9 +146,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||||
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -156,21 +156,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.7",
|
||||||
"@babel/helper-compilation-targets": "^7.28.6",
|
"@babel/helper-compilation-targets": "^7.29.7",
|
||||||
"@babel/helper-module-transforms": "^7.28.6",
|
"@babel/helper-module-transforms": "^7.29.7",
|
||||||
"@babel/helpers": "^7.28.6",
|
"@babel/helpers": "^7.29.7",
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.7",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.29.7",
|
||||||
"@babel/traverse": "^7.29.0",
|
"@babel/traverse": "^7.29.7",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.29.7",
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
|
|
@ -233,14 +233,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets": {
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||||
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.28.6",
|
"@babel/compat-data": "^7.29.7",
|
||||||
"@babel/helper-validator-option": "^7.27.1",
|
"@babel/helper-validator-option": "^7.29.7",
|
||||||
"browserslist": "^4.24.0",
|
"browserslist": "^4.24.0",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
|
|
@ -482,9 +482,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-option": {
|
"node_modules/@babel/helper-validator-option": {
|
||||||
"version": "7.27.1",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -507,14 +507,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||||
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
|
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.29.7",
|
||||||
"@babel/types": "^7.28.6"
|
"@babel/types": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
|
@ -2481,9 +2481,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mediapipe/tasks-vision": {
|
"node_modules/@mediapipe/tasks-vision": {
|
||||||
"version": "0.10.8",
|
"version": "0.10.17",
|
||||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
|
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||||
"integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==",
|
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
|
|
@ -3239,28 +3239,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-spring/animated": {
|
"node_modules/@react-spring/animated": {
|
||||||
"version": "9.6.1",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
|
||||||
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
|
"integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/shared": "~9.6.1",
|
"@react-spring/shared": "~9.7.5",
|
||||||
"@react-spring/types": "~9.6.1"
|
"@react-spring/types": "~9.7.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-spring/core": {
|
"node_modules/@react-spring/core": {
|
||||||
"version": "9.6.1",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
|
||||||
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
|
"integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/animated": "~9.6.1",
|
"@react-spring/animated": "~9.7.5",
|
||||||
"@react-spring/rafz": "~9.6.1",
|
"@react-spring/shared": "~9.7.5",
|
||||||
"@react-spring/shared": "~9.6.1",
|
"@react-spring/types": "~9.7.5"
|
||||||
"@react-spring/types": "~9.6.1"
|
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -3271,34 +3270,34 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-spring/rafz": {
|
"node_modules/@react-spring/rafz": {
|
||||||
"version": "9.6.1",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
|
||||||
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
|
"integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@react-spring/shared": {
|
"node_modules/@react-spring/shared": {
|
||||||
"version": "9.6.1",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
|
||||||
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
|
"integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/rafz": "~9.6.1",
|
"@react-spring/rafz": "~9.7.5",
|
||||||
"@react-spring/types": "~9.6.1"
|
"@react-spring/types": "~9.7.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-spring/three": {
|
"node_modules/@react-spring/three": {
|
||||||
"version": "9.6.1",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
|
||||||
"integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
|
"integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/animated": "~9.6.1",
|
"@react-spring/animated": "~9.7.5",
|
||||||
"@react-spring/core": "~9.6.1",
|
"@react-spring/core": "~9.7.5",
|
||||||
"@react-spring/shared": "~9.6.1",
|
"@react-spring/shared": "~9.7.5",
|
||||||
"@react-spring/types": "~9.6.1"
|
"@react-spring/types": "~9.7.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-three/fiber": ">=6.0",
|
"@react-three/fiber": ">=6.0",
|
||||||
|
|
@ -3307,45 +3306,44 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-spring/types": {
|
"node_modules/@react-spring/types": {
|
||||||
"version": "9.6.1",
|
"version": "9.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
|
||||||
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
|
"integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@react-three/drei": {
|
"node_modules/@react-three/drei": {
|
||||||
"version": "9.105.6",
|
"version": "9.122.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.105.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz",
|
||||||
"integrity": "sha512-JBgYeV36N9N9f1c3o1ZfLYW4rXZA7UQTq32Y8s3DEF6lwj1/y+RP/yq2VG5I8OzUPl7gsmWdy8fpWZgrlAqUpQ==",
|
"integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.11.2",
|
"@babel/runtime": "^7.26.0",
|
||||||
"@mediapipe/tasks-vision": "0.10.8",
|
"@mediapipe/tasks-vision": "0.10.17",
|
||||||
"@monogrid/gainmap-js": "^3.0.5",
|
"@monogrid/gainmap-js": "^3.0.6",
|
||||||
"@react-spring/three": "~9.6.1",
|
"@react-spring/three": "~9.7.5",
|
||||||
"@use-gesture/react": "^10.2.24",
|
"@use-gesture/react": "^10.3.1",
|
||||||
"camera-controls": "^2.4.2",
|
"camera-controls": "^2.9.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"detect-gpu": "^5.0.28",
|
"detect-gpu": "^5.0.56",
|
||||||
"glsl-noise": "^0.0.0",
|
"glsl-noise": "^0.0.0",
|
||||||
"hls.js": "1.3.5",
|
"hls.js": "^1.5.17",
|
||||||
"maath": "^0.10.7",
|
"maath": "^0.10.8",
|
||||||
"meshline": "^3.1.6",
|
"meshline": "^3.3.1",
|
||||||
"react-composer": "^5.0.3",
|
"react-composer": "^5.0.3",
|
||||||
"stats-gl": "^2.0.0",
|
"stats-gl": "^2.2.8",
|
||||||
"stats.js": "^0.17.0",
|
"stats.js": "^0.17.0",
|
||||||
"suspend-react": "^0.1.3",
|
"suspend-react": "^0.1.3",
|
||||||
"three-mesh-bvh": "^0.7.0",
|
"three-mesh-bvh": "^0.7.8",
|
||||||
"three-stdlib": "^2.29.9",
|
"three-stdlib": "^2.35.6",
|
||||||
"troika-three-text": "^0.49.0",
|
"troika-three-text": "^0.52.0",
|
||||||
"tunnel-rat": "^0.1.2",
|
"tunnel-rat": "^0.1.2",
|
||||||
"utility-types": "^3.10.0",
|
"utility-types": "^3.11.0",
|
||||||
"uuid": "^9.0.1",
|
"zustand": "^5.0.1"
|
||||||
"zustand": "^3.7.1"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@react-three/fiber": ">=8.0",
|
"@react-three/fiber": "^8",
|
||||||
"react": ">=18.0",
|
"react": "^18",
|
||||||
"react-dom": ">=18.0",
|
"react-dom": "^18",
|
||||||
"three": ">=0.137"
|
"three": ">=0.137"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
|
@ -3366,6 +3364,35 @@
|
||||||
"three": ">= 0.159.0"
|
"three": ">= 0.159.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-three/drei/node_modules/zustand": {
|
||||||
|
"version": "5.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
|
||||||
|
"integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-three/fiber": {
|
"node_modules/@react-three/fiber": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
|
||||||
|
|
@ -6986,9 +7013,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.17.0",
|
"version": "1.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||||
"integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
|
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.16.0",
|
"follow-redirects": "^1.16.0",
|
||||||
|
|
@ -6998,16 +7025,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios/node_modules/form-data": {
|
"node_modules/axios/node_modules/form-data": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.4",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
|
|
@ -8050,9 +8077,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/deeks": {
|
"node_modules/deeks": {
|
||||||
"version": "3.1.0",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/deeks/-/deeks-3.2.1.tgz",
|
||||||
"integrity": "sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==",
|
"integrity": "sha512-D/o0k3pCG1aI1cxb/dDiWmtMc4Rh7ZQBybXpfMsw9Rbtqwg8kUA9SpYkWcw0pAUjZSnPm8MluctiS0o68r69jQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
|
|
@ -8182,9 +8209,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/doc-path": {
|
"node_modules/doc-path": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.4.tgz",
|
||||||
"integrity": "sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==",
|
"integrity": "sha512-yw5D++UCIB6a033PvQaUvSpW2QuKW0+DOId763n0Q4z3brxS7G8oQr8yBQ1nQFkognKrAVrV6I55TLeU9cfXTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
|
|
@ -8357,6 +8384,25 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-abstract-get": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.2",
|
||||||
|
"is-callable": "^1.2.7",
|
||||||
|
"object-inspect": "^1.13.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-cookie": {
|
"node_modules/es-cookie": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz",
|
||||||
|
|
@ -8416,15 +8462,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-to-primitive": {
|
"node_modules/es-to-primitive": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
|
||||||
"integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
|
"integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"es-abstract-get": "^1.0.0",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"is-callable": "^1.2.7",
|
"is-callable": "^1.2.7",
|
||||||
"is-date-object": "^1.0.5",
|
"is-date-object": "^1.1.0",
|
||||||
"is-symbol": "^1.0.4"
|
"is-symbol": "^1.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -8764,9 +8813,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -8977,15 +9026,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz",
|
||||||
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
|
"integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.4",
|
||||||
"mime-types": "^2.1.35"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -9060,18 +9109,21 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/function.prototype.name": {
|
"node_modules/function.prototype.name": {
|
||||||
"version": "1.1.8",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
|
||||||
"integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
|
"integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind": "^1.0.8",
|
"call-bind": "^1.0.9",
|
||||||
"call-bound": "^1.0.3",
|
"call-bound": "^1.0.4",
|
||||||
"define-properties": "^1.2.1",
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"functions-have-names": "^1.2.3",
|
"functions-have-names": "^1.2.3",
|
||||||
"hasown": "^2.0.2",
|
"has-property-descriptors": "^1.0.2",
|
||||||
"is-callable": "^1.2.7"
|
"hasown": "^2.0.4",
|
||||||
|
"is-callable": "^1.2.7",
|
||||||
|
"is-document.all": "^1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -9316,9 +9368,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -9535,9 +9587,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|
@ -9547,9 +9599,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hls.js": {
|
"node_modules/hls.js": {
|
||||||
"version": "1.3.5",
|
"version": "1.6.16",
|
||||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
|
||||||
"integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==",
|
"integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/hoist-non-react-statics": {
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
|
@ -9913,6 +9965,22 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-document.all": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extendable": {
|
"node_modules/is-extendable": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||||
|
|
@ -10356,10 +10424,20 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
|
|
@ -10381,13 +10459,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/json-2-csv": {
|
"node_modules/json-2-csv": {
|
||||||
"version": "5.5.10",
|
"version": "5.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.11.tgz",
|
||||||
"integrity": "sha512-Dep8wO3Fr5wNjQevO2Z8Y7yeee/nYSGRsi7q6zJDKEVHxXkXT+v21vxHmDX923UzmCXXkSo62HaTz6eTWzFLaw==",
|
"integrity": "sha512-kVuwgVL7rfad9ETf02ZZxJPuMR5ZSUn139+T34BfmVxYhb/IsAIm/LzEeQ8YLJmXfuQ5z7LUAFrgPZZM6VLJPw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deeks": "3.1.0",
|
"deeks": "3.2.1",
|
||||||
"doc-path": "4.1.1"
|
"doc-path": "4.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
|
|
@ -11631,9 +11709,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.15",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -11666,9 +11744,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/postcss/node_modules/nanoid": {
|
"node_modules/postcss/node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.15",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -11752,7 +11830,7 @@
|
||||||
},
|
},
|
||||||
"node_modules/protobuf-ts": {
|
"node_modules/protobuf-ts": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#9c0f668d4a56b8216dd71a44c3110a818aaf3541",
|
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#d6a992e46192a8ab8195235357318dc297738064",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"protobufjs": "^6.8.8"
|
"protobufjs": "^6.8.8"
|
||||||
}
|
}
|
||||||
|
|
@ -12903,9 +12981,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serialize-javascript": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz",
|
||||||
"integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
|
"integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -13681,9 +13759,10 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/three-mesh-bvh": {
|
"node_modules/three-mesh-bvh": {
|
||||||
"version": "0.7.6",
|
"version": "0.7.8",
|
||||||
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz",
|
||||||
"integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==",
|
"integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==",
|
||||||
|
"deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"three": ">= 0.151.0"
|
"three": ">= 0.151.0"
|
||||||
|
|
@ -13861,14 +13940,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/troika-three-text": {
|
"node_modules/troika-three-text": {
|
||||||
"version": "0.49.1",
|
"version": "0.52.4",
|
||||||
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz",
|
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||||
"integrity": "sha512-lXGWxgjJP9kw4i4Wh+0k0Q/7cRfS6iOME4knKht/KozPu9GcFA9NnNpRvehIhrUawq9B0ZRw+0oiFHgRO+4Wig==",
|
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bidi-js": "^1.0.2",
|
"bidi-js": "^1.0.2",
|
||||||
"troika-three-utils": "^0.49.0",
|
"troika-three-utils": "^0.52.4",
|
||||||
"troika-worker-utils": "^0.49.0",
|
"troika-worker-utils": "^0.52.0",
|
||||||
"webgl-sdf-generator": "1.1.1"
|
"webgl-sdf-generator": "1.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
@ -13876,18 +13955,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/troika-three-utils": {
|
"node_modules/troika-three-utils": {
|
||||||
"version": "0.49.0",
|
"version": "0.52.4",
|
||||||
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||||
"integrity": "sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA==",
|
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"three": ">=0.125.0"
|
"three": ">=0.125.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/troika-worker-utils": {
|
"node_modules/troika-worker-utils": {
|
||||||
"version": "0.49.0",
|
"version": "0.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz",
|
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||||
"integrity": "sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg==",
|
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
|
|
@ -14341,19 +14420,6 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
|
||||||
"funding": [
|
|
||||||
"https://github.com/sponsors/broofa",
|
|
||||||
"https://github.com/sponsors/ctavan"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/validate-npm-package-license": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
"mui-tel-input": "^7.0.0",
|
"mui-tel-input": "^7.0.0",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
"openweathermap-ts": "^1.2.10",
|
"openweathermap-ts": "^1.2.10",
|
||||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#sendgrid_whitelabel",
|
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
|
||||||
"query-string": "^9.2.1",
|
"query-string": "^9.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const useStyles = makeStyles((theme: Theme) => {
|
||||||
appBar: {
|
appBar: {
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
backgroundImage: "none", // This prevents de-saturation of header in dark mode
|
backgroundImage: "none", // This prevents de-saturation of header in dark mode
|
||||||
backgroundColor: theme.palette.mode === "light" ? "#3b3b3b" : "#272727"
|
backgroundColor: theme.palette.header.main
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
marginLeft: theme.spacing(0.5),
|
marginLeft: theme.spacing(0.5),
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Bin } from "models";
|
||||||
import moment, { Moment } from "moment";
|
import moment, { Moment } from "moment";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { quack } from "protobuf-ts/quack";
|
import { quack } from "protobuf-ts/quack";
|
||||||
import { useComponentAPI } from "providers";
|
import { useBinAPI, useComponentAPI } from "providers";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
const CHECKPOINT_COUNT = 10;
|
const CHECKPOINT_COUNT = 10;
|
||||||
|
|
@ -16,6 +16,7 @@ const CHECKPOINT_COUNT = 10;
|
||||||
interface StatusCheckpoint {
|
interface StatusCheckpoint {
|
||||||
time: Moment;
|
time: Moment;
|
||||||
timeString: string;
|
timeString: string;
|
||||||
|
statusBushels: number;
|
||||||
cables: pond.GrainCable[];
|
cables: pond.GrainCable[];
|
||||||
fans: pond.BinFan[];
|
fans: pond.BinFan[];
|
||||||
heaters: pond.BinHeater[];
|
heaters: pond.BinHeater[];
|
||||||
|
|
@ -90,6 +91,103 @@ function upsertReading(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trimmed record of a single component-settings history entry: just the timestamp and the
|
||||||
|
* top-node value (`grain_filled_to`) that was in effect from that point forward.
|
||||||
|
*/
|
||||||
|
type TopNodeHistoryEntry = {
|
||||||
|
timestamp: Moment;
|
||||||
|
topNode: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildTopNodeHistory(
|
||||||
|
history: pond.ComponentHistory[]
|
||||||
|
): TopNodeHistoryEntry[] {
|
||||||
|
return history
|
||||||
|
.filter(h => h.component && h.component.grainFilledTo > 0 && h.timestamp)
|
||||||
|
.map(h => ({
|
||||||
|
timestamp: moment(h.timestamp),
|
||||||
|
topNode: h.component ? h.component.grainFilledTo : 0,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
function topNodeAtTime(
|
||||||
|
history: TopNodeHistoryEntry[],
|
||||||
|
checkpointTime: Moment
|
||||||
|
): number | undefined {
|
||||||
|
if (history.length === 0) return undefined;
|
||||||
|
const t = checkpointTime.valueOf();
|
||||||
|
let lo = 0;
|
||||||
|
let hi = history.length - 1;
|
||||||
|
let result: number | undefined = undefined;
|
||||||
|
while (lo <= hi) {
|
||||||
|
const mid = (lo + hi) >>> 1;
|
||||||
|
if (history[mid].timestamp.valueOf() <= t) {
|
||||||
|
result = history[mid].topNode;
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
hi = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single recorded bushel estimate with its timestamp, sourced from bin object measurements.
|
||||||
|
*/
|
||||||
|
type BushelEntry = {
|
||||||
|
timestamp: Moment;
|
||||||
|
bushels: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildBushelHistory(
|
||||||
|
response: pond.ListObjectMeasurementsResponse
|
||||||
|
): BushelEntry[] {
|
||||||
|
const entries: BushelEntry[] = [];
|
||||||
|
|
||||||
|
response.measurements.forEach(um => {
|
||||||
|
const isBushelType =
|
||||||
|
um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_LIDAR ||
|
||||||
|
um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_CABLE ||
|
||||||
|
um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_LIBRACART;
|
||||||
|
|
||||||
|
if (!isBushelType) return;
|
||||||
|
|
||||||
|
um.timestamps.forEach((ts, i) => {
|
||||||
|
const valueArray = um.values[i];
|
||||||
|
if (!ts || !valueArray || valueArray.error || valueArray.values.length === 0) return;
|
||||||
|
entries.push({
|
||||||
|
timestamp: moment(ts),
|
||||||
|
bushels: valueArray.values[0],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
function bushelsAtTime(
|
||||||
|
history: BushelEntry[],
|
||||||
|
checkpointTime: Moment
|
||||||
|
): number | undefined {
|
||||||
|
if (history.length === 0) return undefined;
|
||||||
|
const t = checkpointTime.valueOf();
|
||||||
|
let lo = 0;
|
||||||
|
let hi = history.length - 1;
|
||||||
|
let result: number | undefined = undefined;
|
||||||
|
while (lo <= hi) {
|
||||||
|
const mid = (lo + hi) >>> 1;
|
||||||
|
if (history[mid].timestamp.valueOf() <= t) {
|
||||||
|
result = history[mid].bushels;
|
||||||
|
lo = mid + 1;
|
||||||
|
} else {
|
||||||
|
hi = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Produces `count` wall-clock times evenly spaced from `start` through `end` (inclusive).
|
* Produces `count` wall-clock times evenly spaced from `start` through `end` (inclusive).
|
||||||
* These are the playback frames — not derived from measurement data.
|
* These are the playback frames — not derived from measurement data.
|
||||||
|
|
@ -360,6 +458,12 @@ function buildStatusCheckpoints(
|
||||||
cableMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
|
cableMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
|
||||||
fanMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
|
fanMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
|
||||||
heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
|
heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[],
|
||||||
|
/** Sorted top-node history per cable key. Pass an empty Map when not needed. */
|
||||||
|
topNodeHistories: Map<string, TopNodeHistoryEntry[]>,
|
||||||
|
/** Sorted bushel history derived from bin object measurements. */
|
||||||
|
bushelHistory: BushelEntry[],
|
||||||
|
/** Fallback bushel count used when no measurement precedes a checkpoint. */
|
||||||
|
fallbackBushels: number,
|
||||||
start: Moment,
|
start: Moment,
|
||||||
end: Moment,
|
end: Moment,
|
||||||
count: number = CHECKPOINT_COUNT
|
count: number = CHECKPOINT_COUNT
|
||||||
|
|
@ -416,7 +520,19 @@ function buildStatusCheckpoints(
|
||||||
const bucketReadings = cableBucket.get(key) ?? {};
|
const bucketReadings = cableBucket.get(key) ?? {};
|
||||||
const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings);
|
const merged = mergeCarriedReadings(carriedByCable.get(key)!, bucketReadings);
|
||||||
carriedByCable.set(key, merged);
|
carriedByCable.set(key, merged);
|
||||||
return buildGrainCableFromReadings(template, merged, time);
|
const cable = buildGrainCableFromReadings(template, merged, time);
|
||||||
|
|
||||||
|
// Apply the most recent top-node setting that was in effect at this checkpoint time.
|
||||||
|
// Falls back to the template's current top_node when no history entry precedes this time.
|
||||||
|
const history = topNodeHistories.get(key);
|
||||||
|
if (history) {
|
||||||
|
const tn = topNodeAtTime(history, time);
|
||||||
|
if (tn !== undefined) {
|
||||||
|
cable.topNode = tn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cable;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build fans
|
// Build fans
|
||||||
|
|
@ -437,7 +553,12 @@ function buildStatusCheckpoints(
|
||||||
return buildBinHeaterFromState(template, merged);
|
return buildBinHeaterFromState(template, merged);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { time, cables, fans, heaters, timeString: time.toISOString() };
|
// Resolve bushels: binary search gives the most recent recorded value at or before this
|
||||||
|
// checkpoint (carry-forward is implicit in the search). Falls back to the current live
|
||||||
|
// status bushels when the playback range predates all recorded measurements.
|
||||||
|
const statusBushels = bushelsAtTime(bushelHistory, time) ?? fallbackBushels;
|
||||||
|
|
||||||
|
return { time, cables, fans, heaters, timeString: time.toISOString(), statusBushels };
|
||||||
}).filter(checkpoint =>
|
}).filter(checkpoint =>
|
||||||
// Keep checkpoints that have at least some cable data
|
// Keep checkpoints that have at least some cable data
|
||||||
checkpoint.cables.some(cable =>
|
checkpoint.cables.some(cable =>
|
||||||
|
|
@ -471,6 +592,7 @@ export default function BinPlayer(props: Props) {
|
||||||
const { bin, componentDevices, setBin } = props;
|
const { bin, componentDevices, setBin } = props;
|
||||||
const isMobile = useMobile();
|
const isMobile = useMobile();
|
||||||
const componentAPI = useComponentAPI();
|
const componentAPI = useComponentAPI();
|
||||||
|
const binAPI = useBinAPI();
|
||||||
const [dateSelect, setDateSelect] = useState<number>(0);
|
const [dateSelect, setDateSelect] = useState<number>(0);
|
||||||
const [startDate, setStartDate] = useState<Moment>(moment().subtract(1, 'week'));
|
const [startDate, setStartDate] = useState<Moment>(moment().subtract(1, 'week'));
|
||||||
const [endDate, setEndDate] = useState<Moment>(moment);
|
const [endDate, setEndDate] = useState<Moment>(moment);
|
||||||
|
|
@ -529,6 +651,17 @@ export default function BinPlayer(props: Props) {
|
||||||
return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
|
return componentAPI.sampleUnitMeasurements(deviceID, cable.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cable top-node history requests — one per cable, covering the full playback range.
|
||||||
|
const cableHistoryRequests = bin.status.grainCables.map(cable => {
|
||||||
|
const deviceID = componentDevices.get(cable.key) ?? 0;
|
||||||
|
return componentAPI.listHistoryBetween(
|
||||||
|
deviceID,
|
||||||
|
cable.key,
|
||||||
|
startDate.toISOString(),
|
||||||
|
endDate.toISOString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Fan requests
|
// Fan requests
|
||||||
const fanSampleRequests = bin.status.fans.map(fan => {
|
const fanSampleRequests = bin.status.fans.map(fan => {
|
||||||
const deviceID = componentDevices.get(fan.key) ?? 0;
|
const deviceID = componentDevices.get(fan.key) ?? 0;
|
||||||
|
|
@ -541,12 +674,27 @@ export default function BinPlayer(props: Props) {
|
||||||
return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
|
return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [cableResponses, fanResponses, heaterResponses] = await Promise.all([
|
const binMeasurementsRequest = binAPI.listBinMeasurements(bin.key(), startDate, endDate, 0, 0, 'asc');
|
||||||
|
|
||||||
|
const [cableResponses, fanResponses, heaterResponses, cableHistoryResponses, binMeasurementsResponse] = await Promise.all([
|
||||||
Promise.all(cableSampleRequests),
|
Promise.all(cableSampleRequests),
|
||||||
Promise.all(fanSampleRequests),
|
Promise.all(fanSampleRequests),
|
||||||
Promise.all(heaterSampleRequests),
|
Promise.all(heaterSampleRequests),
|
||||||
|
Promise.all(cableHistoryRequests),
|
||||||
|
binMeasurementsRequest,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Build a per-cable sorted top-node history map for use during checkpoint construction.
|
||||||
|
const topNodeHistories = new Map<string, TopNodeHistoryEntry[]>();
|
||||||
|
bin.status.grainCables.forEach((cable, i) => {
|
||||||
|
const historyResp = cableHistoryResponses[i];
|
||||||
|
const entries = buildTopNodeHistory(historyResp?.data?.history ?? []);
|
||||||
|
topNodeHistories.set(cable.key, entries);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build sorted bushel history from bin object measurements.
|
||||||
|
const bushelHistory = buildBushelHistory(binMeasurementsResponse.data);
|
||||||
|
|
||||||
const checkpoints = buildStatusCheckpoints(
|
const checkpoints = buildStatusCheckpoints(
|
||||||
bin.status.grainCables,
|
bin.status.grainCables,
|
||||||
bin.status.fans,
|
bin.status.fans,
|
||||||
|
|
@ -554,6 +702,9 @@ export default function BinPlayer(props: Props) {
|
||||||
cableResponses.map(r => r.data),
|
cableResponses.map(r => r.data),
|
||||||
fanResponses.map(r => r.data),
|
fanResponses.map(r => r.data),
|
||||||
heaterResponses.map(r => r.data),
|
heaterResponses.map(r => r.data),
|
||||||
|
topNodeHistories,
|
||||||
|
bushelHistory,
|
||||||
|
bin.status.grainBushels,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
checkpointCountFromRange(startDate, endDate)
|
checkpointCountFromRange(startDate, endDate)
|
||||||
|
|
@ -574,6 +725,7 @@ export default function BinPlayer(props: Props) {
|
||||||
newBin.status.grainCables = checkpoints[i].cables;
|
newBin.status.grainCables = checkpoints[i].cables;
|
||||||
newBin.status.fans = checkpoints[i].fans;
|
newBin.status.fans = checkpoints[i].fans;
|
||||||
newBin.status.heaters = checkpoints[i].heaters;
|
newBin.status.heaters = checkpoints[i].heaters;
|
||||||
|
newBin.status.grainBushels = checkpoints[i].statusBushels;
|
||||||
|
|
||||||
setBin(newBin);
|
setBin(newBin);
|
||||||
setCurrentCheckpointTime(checkpoints[i].time);
|
setCurrentCheckpointTime(checkpoints[i].time);
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,7 @@ export default function BinTableView(props: Props) {
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ background: theme.palette.background.default}}
|
sx={{ background: theme.palette.background.default, color: theme.palette.text.primary}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenExpandedCables(true)
|
setOpenExpandedCables(true)
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,11 @@ export default function ChatOutput(props: Props) {
|
||||||
|
|
||||||
const ScrollToBottom = () => {
|
const ScrollToBottom = () => {
|
||||||
const elementRef = useRef<null | HTMLDivElement>(null);
|
const elementRef = useRef<null | HTMLDivElement>(null);
|
||||||
useEffect(() =>
|
useEffect(() => {
|
||||||
elementRef?.current?.scrollIntoView({
|
elementRef?.current?.scrollIntoView({
|
||||||
block: "end"
|
block: "end"
|
||||||
})
|
});
|
||||||
);
|
});
|
||||||
let elem = <div ref={elementRef} />;
|
let elem = <div ref={elementRef} />;
|
||||||
return elem;
|
return elem;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
46
src/common/PendingChangesChip.tsx
Normal file
46
src/common/PendingChangesChip.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Chip, CircularProgress, Tooltip } from "@mui/material";
|
||||||
|
import { CheckCircleOutline } from "@mui/icons-material";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pending: boolean;
|
||||||
|
accepted: boolean;
|
||||||
|
size?: "small" | "medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capsule tag shown alongside the other status chips while changes are
|
||||||
|
* waiting to be synced to a device or component. Shows a small loading
|
||||||
|
* circle while pending and a checkmark once the change is accepted.
|
||||||
|
* Derive the flags with usePendingChanges.
|
||||||
|
*/
|
||||||
|
export default function PendingChangesChip(props: Props) {
|
||||||
|
const { pending, accepted, size } = props;
|
||||||
|
|
||||||
|
if (pending) {
|
||||||
|
return (
|
||||||
|
<Tooltip title="Your most recent changes will be synced to the device as soon as possible">
|
||||||
|
<Chip
|
||||||
|
variant="outlined"
|
||||||
|
label="Pending changes"
|
||||||
|
icon={<CircularProgress size={14} thickness={5} color="inherit" />}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accepted) {
|
||||||
|
return (
|
||||||
|
<Tooltip title="The device has accepted your most recent changes">
|
||||||
|
<Chip
|
||||||
|
variant="outlined"
|
||||||
|
label="Settings synced"
|
||||||
|
icon={<CheckCircleOutline style={{ color: "var(--status-ok)" }} />}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
27
src/common/PendingChangesIndicator.tsx
Normal file
27
src/common/PendingChangesIndicator.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { CircularProgress, Grid2 as Grid } from "@mui/material";
|
||||||
|
import { CheckCircleOutline } from "@mui/icons-material";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pending: boolean;
|
||||||
|
accepted: boolean;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small inline status line used inside cards: a loading circle beside the
|
||||||
|
* caption while changes are pending, and a checkmark once they are accepted.
|
||||||
|
* Derive the flags with usePendingChanges (or per-item tracking like
|
||||||
|
* InteractionsOverview does).
|
||||||
|
*/
|
||||||
|
export default function PendingChangesIndicator(props: Props) {
|
||||||
|
const { pending, accepted, text } = props;
|
||||||
|
if (!pending && !accepted) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={0.5} alignItems="center">
|
||||||
|
{pending && <CircularProgress size={10} thickness={5} color="inherit" />}
|
||||||
|
{accepted && <CheckCircleOutline sx={{ fontSize: 13, color: "var(--status-ok)" }} />}
|
||||||
|
<Grid>{text}</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { Chip, Tooltip } from "@mui/material";
|
|
||||||
import { getStatusHelper } from "pbHelpers/Status";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
status: string;
|
|
||||||
size?: "small" | "medium";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function StatusChip(props: Props) {
|
|
||||||
const { status, size } = props;
|
|
||||||
const statusHelper = getStatusHelper(status);
|
|
||||||
const icon = statusHelper.icon;
|
|
||||||
const description = statusHelper.description;
|
|
||||||
|
|
||||||
return icon !== null ? (
|
|
||||||
<Tooltip title="Your most recent changes will be synced to the device as soon as possible">
|
|
||||||
<Chip variant="outlined" label={description} icon={icon} size={size} />
|
|
||||||
</Tooltip>
|
|
||||||
) : null;
|
|
||||||
}
|
|
||||||
|
|
@ -13,11 +13,13 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import Grid from '@mui/material/Grid2';
|
import Grid from '@mui/material/Grid2';
|
||||||
import EventBlocker from "common/EventBlocker";
|
import EventBlocker from "common/EventBlocker";
|
||||||
|
import PendingChangesIndicator from "common/PendingChangesIndicator";
|
||||||
import MeasurementSummary from "component/MeasurementSummary";
|
import MeasurementSummary from "component/MeasurementSummary";
|
||||||
import { useComponentAPI, useSnackbar, useThemeType } from "hooks";
|
import { useComponentAPI, usePendingChanges, useSnackbar, useThemeType } from "hooks";
|
||||||
import InteractionsOverview from "interactions/InteractionsOverview";
|
import InteractionsOverview from "interactions/InteractionsOverview";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { Component, Device, Interaction } from "models";
|
import { Component, Device, Interaction } from "models";
|
||||||
|
import moment from "moment";
|
||||||
import { getFriendlyAddressTypeName, getHumanReadableAddress } from "pbHelpers/AddressType";
|
import { getFriendlyAddressTypeName, getHumanReadableAddress } from "pbHelpers/AddressType";
|
||||||
import { controllerModeLabel } from "pbHelpers/Component";
|
import { controllerModeLabel } from "pbHelpers/Component";
|
||||||
import {
|
import {
|
||||||
|
|
@ -121,6 +123,9 @@ export default function ComponentCard(props: Props) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const [sensors, setSensors] = useState<Sensor[]>([]);
|
const [sensors, setSensors] = useState<Sensor[]>([]);
|
||||||
const [{ user, showErrors, as }] = useGlobalState();
|
const [{ user, showErrors, as }] = useGlobalState();
|
||||||
|
const { pending: pendingChanges, accepted: changesAccepted } = usePendingChanges(
|
||||||
|
component.status.synced
|
||||||
|
);
|
||||||
|
|
||||||
const updateControllerState = (checked: boolean) => {
|
const updateControllerState = (checked: boolean) => {
|
||||||
let updatedComponent = cloneDeep(component);
|
let updatedComponent = cloneDeep(component);
|
||||||
|
|
@ -133,6 +138,8 @@ export default function ComponentCard(props: Props) {
|
||||||
.update(device.id(), updatedComponent.settings, undefined, undefined, as)
|
.update(device.id(), updatedComponent.settings, undefined, undefined, as)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
success(component.name() + "'s mode was set to " + describe);
|
success(component.name() + "'s mode was set to " + describe);
|
||||||
|
updatedComponent.status.synced = false;
|
||||||
|
updatedComponent.status.lastUpdate = moment().toISOString();
|
||||||
refreshCallback(updatedComponent);
|
refreshCallback(updatedComponent);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|
@ -287,19 +294,34 @@ export default function ComponentCard(props: Props) {
|
||||||
className={classes.cardHeader}
|
className={classes.cardHeader}
|
||||||
titleTypographyProps={{ variant: "subtitle1" }}
|
titleTypographyProps={{ variant: "subtitle1" }}
|
||||||
subheader={
|
subheader={
|
||||||
// !newStructure ? (
|
<React.Fragment>
|
||||||
// <MeasurementSummary
|
{(pendingChanges || changesAccepted) && (
|
||||||
// component={component}
|
<Typography variant="caption" color="textSecondary" component="div">
|
||||||
// reading={component.status.lastMeasurement}
|
<PendingChangesIndicator
|
||||||
// dense
|
pending={pendingChanges}
|
||||||
// />
|
accepted={changesAccepted}
|
||||||
// ) : (
|
text={
|
||||||
|
changesAccepted
|
||||||
|
? "Pending change accepted"
|
||||||
|
: "Pending " + moment(component.status.lastUpdate).fromNow()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{/* !newStructure ? (
|
||||||
|
<MeasurementSummary
|
||||||
|
component={component}
|
||||||
|
reading={component.status.lastMeasurement}
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
) : ( */}
|
||||||
<UnitMeasurementSummary
|
<UnitMeasurementSummary
|
||||||
component={component}
|
component={component}
|
||||||
reading={UnitMeasurement.convertLastMeasurement(measurements)}
|
reading={UnitMeasurement.convertLastMeasurement(measurements)}
|
||||||
//dense
|
//dense
|
||||||
/>
|
/>
|
||||||
// )
|
{/* ) */}
|
||||||
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
action={
|
action={
|
||||||
<EventBlocker>
|
<EventBlocker>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
ExpandMore,
|
ExpandMore,
|
||||||
Close as CloseIcon
|
Close as CloseIcon,
|
||||||
|
Remove as RemoveIcon
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import PeriodSelect from "common/time/PeriodSelect";
|
import PeriodSelect from "common/time/PeriodSelect";
|
||||||
import SearchSelect, { Option } from "common/SearchSelect";
|
import SearchSelect, { Option } from "common/SearchSelect";
|
||||||
|
|
@ -97,6 +98,7 @@ interface Props {
|
||||||
component: Component;
|
component: Component;
|
||||||
componentChanged: (component: Component, formValid: boolean) => void;
|
componentChanged: (component: Component, formValid: boolean) => void;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
hideControllerFields?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentData {
|
interface ComponentData {
|
||||||
|
|
@ -119,7 +121,7 @@ export default function ComponentForm(props: Props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const grainOptions = GrainOptions();
|
const grainOptions = GrainOptions();
|
||||||
const [{ user }] = useGlobalState();
|
const [{ user }] = useGlobalState();
|
||||||
const { device, component, componentChanged, canEdit } = props;
|
const { device, component, componentChanged, canEdit, hideControllerFields } = props;
|
||||||
const [reportExpanded, setReportExpanded] = useState(false);
|
const [reportExpanded, setReportExpanded] = useState(false);
|
||||||
const [dataUsageWarningDismissed, setDataUsageWarningDismissed] = useState(true);
|
const [dataUsageWarningDismissed, setDataUsageWarningDismissed] = useState(true);
|
||||||
const [form, setForm] = useState<ComponentData>({
|
const [form, setForm] = useState<ComponentData>({
|
||||||
|
|
@ -137,7 +139,8 @@ export default function ComponentForm(props: Props) {
|
||||||
sensorDistance: "0",
|
sensorDistance: "0",
|
||||||
});
|
});
|
||||||
const [compMode, setCompMode] = useState<any>();
|
const [compMode, setCompMode] = useState<any>();
|
||||||
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false)
|
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false);
|
||||||
|
const [controlEmail, setControlEmail] = useState("");
|
||||||
//const [numCalibrations, setNumCalibrations] = useState(0)
|
//const [numCalibrations, setNumCalibrations] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -356,6 +359,21 @@ export default function ComponentForm(props: Props) {
|
||||||
setForm(f);
|
setForm(f);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addControlEmail = () => {
|
||||||
|
const trimmed = controlEmail.trim().toLowerCase();
|
||||||
|
if (trimmed === "" || form.component.settings.controlEmails.includes(trimmed)) return;
|
||||||
|
let f = cloneDeep(form);
|
||||||
|
f.component.settings.controlEmails.push(trimmed);
|
||||||
|
setForm(f);
|
||||||
|
setControlEmail("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeControlEmail = (index: number) => {
|
||||||
|
let f = cloneDeep(form);
|
||||||
|
f.component.settings.controlEmails.splice(index, 1);
|
||||||
|
setForm(f);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMeasure = (event: any) => {
|
const toggleMeasure = (event: any) => {
|
||||||
let f = cloneDeep(form);
|
let f = cloneDeep(form);
|
||||||
f.measure = event.target.checked;
|
f.measure = event.target.checked;
|
||||||
|
|
@ -439,11 +457,29 @@ export default function ComponentForm(props: Props) {
|
||||||
setForm(f);
|
setForm(f);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modeDictionaryValue = (dictionary: any, key: string): number | undefined => {
|
||||||
|
if (!Array.isArray(dictionary)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let entry = dictionary.find((elem: any) => elem.key === key);
|
||||||
|
return typeof entry?.value === "number" ? entry.value : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const isCoefficientValid = () => {
|
const isCoefficientValid = () => {
|
||||||
let calibrate = form.component.settings.calibrate;
|
let calibrate = form.component.settings.calibrate;
|
||||||
let coefficient = Number(form.coefficient);
|
let coefficient = Number(form.coefficient);
|
||||||
let valid = !isNaN(coefficient) && (!calibrate || (calibrate && coefficient > 0));
|
if (isNaN(coefficient)) {
|
||||||
return valid;
|
return false;
|
||||||
|
}
|
||||||
|
if (!calibrate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (compMode && compMode.mode.entryA === "number") {
|
||||||
|
let minimum = modeDictionaryValue(compMode.mode.dictionaryA, "min") ?? 0;
|
||||||
|
let maximum = modeDictionaryValue(compMode.mode.dictionaryA, "max") ?? 100;
|
||||||
|
return coefficient >= minimum && coefficient <= maximum;
|
||||||
|
}
|
||||||
|
return coefficient > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeCoefficient = (event: any) => {
|
const changeCoefficient = (event: any) => {
|
||||||
|
|
@ -467,10 +503,14 @@ export default function ComponentForm(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isOffsetValid = (min?: number, max?: number) => {
|
const isOffsetValid = (min?: number, max?: number) => {
|
||||||
let minimum = min ?? 0;
|
const minimum = min ?? modeDictionaryValue(compMode?.mode.dictionaryB, "min") ?? 0;
|
||||||
let maximum = max ?? 100;
|
const maximum = max ?? modeDictionaryValue(compMode?.mode.dictionaryB, "max") ?? 100;
|
||||||
if (compMode && form.component.settings.calibrate) {
|
if (compMode && form.component.settings.calibrate) {
|
||||||
return Number(form.offset) <= maximum && Number(form.offset) >= minimum;
|
const offset = Number(form.offset);
|
||||||
|
const meetsMinimum = compMode.mode.exclusiveMinimumB
|
||||||
|
? offset > minimum
|
||||||
|
: offset >= minimum;
|
||||||
|
return !isNaN(offset) && offset <= maximum && meetsMinimum;
|
||||||
}
|
}
|
||||||
return !isNaN(Number(form.offset));
|
return !isNaN(Number(form.offset));
|
||||||
};
|
};
|
||||||
|
|
@ -814,6 +854,11 @@ export default function ComponentForm(props: Props) {
|
||||||
onFocus={selectText}
|
onFocus={selectText}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
inputProps={
|
||||||
|
compMode.mode.stepB !== undefined
|
||||||
|
? { step: compMode.mode.stepB }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
type={compMode.mode.entryB}
|
type={compMode.mode.entryB}
|
||||||
variant="outlined">
|
variant="outlined">
|
||||||
{compMode.mode.entryB === "select" &&
|
{compMode.mode.entryB === "select" &&
|
||||||
|
|
@ -1341,7 +1386,7 @@ export default function ComponentForm(props: Props) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isController(component.settings.type) && (
|
{isController(component.settings.type) && !hideControllerFields && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Grid size={{ xs: 12 }}>
|
<Grid size={{ xs: 12 }}>
|
||||||
<TextField
|
<TextField
|
||||||
|
|
@ -1382,6 +1427,62 @@ export default function ComponentForm(props: Props) {
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12 }}>
|
||||||
|
<Typography variant="subtitle2" style={{ marginTop: 16, marginBottom: 8 }}>
|
||||||
|
Authorized Control Emails
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={1} alignItems="center">
|
||||||
|
<Grid size={{ xs: 9 }}>
|
||||||
|
<TextField
|
||||||
|
label="Email address"
|
||||||
|
type="email"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
value={controlEmail}
|
||||||
|
onChange={e => setControlEmail(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
addControlEmail();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 3 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={addControlEmail}
|
||||||
|
disabled={!canEdit || controlEmail.trim() === ""}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{component.settings.controlEmails.map((email, i) => (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
key={i}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
style={{ marginTop: 4 }}>
|
||||||
|
<Grid>
|
||||||
|
<Typography variant="body2">{email}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => removeControlEmail(i)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
className={classes.redButton}>
|
||||||
|
<RemoveIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
{showDataUsageWarning && (
|
{showDataUsageWarning && (
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"dictionaryA": [
|
"dictionaryA": [
|
||||||
{
|
{
|
||||||
"key": "min",
|
"key": "min",
|
||||||
"value": 1,
|
"value": 0,
|
||||||
"restricted": false
|
"restricted": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -121,10 +121,12 @@
|
||||||
],
|
],
|
||||||
"labelB": "To Node Id",
|
"labelB": "To Node Id",
|
||||||
"entryB": "number",
|
"entryB": "number",
|
||||||
|
"exclusiveMinimumB": true,
|
||||||
|
"stepB": "any",
|
||||||
"dictionaryB": [
|
"dictionaryB": [
|
||||||
{
|
{
|
||||||
"key": "min",
|
"key": "min",
|
||||||
"value": 1,
|
"value": 0,
|
||||||
"restricted": false
|
"restricted": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Divider,
|
Divider,
|
||||||
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Grid2 as Grid,
|
Grid2 as Grid,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
|
@ -41,8 +42,10 @@ import {
|
||||||
GetComponentTypeOptions,
|
GetComponentTypeOptions,
|
||||||
getFriendlyName,
|
getFriendlyName,
|
||||||
getMeasurements,
|
getMeasurements,
|
||||||
isController
|
isController,
|
||||||
} from "pbHelpers/ComponentType";
|
} from "pbHelpers/ComponentType";
|
||||||
|
import PeriodSelect from "common/time/PeriodSelect";
|
||||||
|
import { bestUnit, milliToX } from "common/time/duration";
|
||||||
import {
|
import {
|
||||||
ComponentAvailabilityMap,
|
ComponentAvailabilityMap,
|
||||||
DeviceAvailabilityMap,
|
DeviceAvailabilityMap,
|
||||||
|
|
@ -177,6 +180,7 @@ export default function ComponentSettings(props: Props) {
|
||||||
const [formValid, setFormValid] = useState(false);
|
const [formValid, setFormValid] = useState(false);
|
||||||
const [excludedNodes, setExcludedNodes] = useState<number[]>([]);
|
const [excludedNodes, setExcludedNodes] = useState<number[]>([]);
|
||||||
const [nodeToExclude, setNodeToExclude] = useState(0);
|
const [nodeToExclude, setNodeToExclude] = useState(0);
|
||||||
|
const [controlEmail, setControlEmail] = useState("");
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||||
setTabVal(newValue);
|
setTabVal(newValue);
|
||||||
|
|
@ -209,14 +213,13 @@ export default function ComponentSettings(props: Props) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
setFormComponent(initComponent);
|
setFormComponent(initComponent);
|
||||||
setExcludedNodes(initComponent.settings.excludedNodes)
|
setExcludedNodes(cloneDeep(initComponent.settings.excludedNodes));
|
||||||
}, [props.component, componentTypeOptions]);
|
}, [props.component, componentTypeOptions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.component !== prevComponent || (!isDialogOpen && prevIsDialogOpen)) {
|
if (!isDialogOpen || prevIsDialogOpen) return;
|
||||||
init();
|
init();
|
||||||
}
|
}, [init, isDialogOpen, prevIsDialogOpen]);
|
||||||
}, [props.component, prevComponent, init, isDialogOpen, prevIsDialogOpen]);
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
closeDialogCallback();
|
closeDialogCallback();
|
||||||
|
|
@ -498,6 +501,7 @@ export default function ComponentSettings(props: Props) {
|
||||||
component={formComponent}
|
component={formComponent}
|
||||||
device={device}
|
device={device}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
|
hideControllerFields={isController(formComponent.settings.type)}
|
||||||
componentChanged={(formComp, isValid) => {
|
componentChanged={(formComp, isValid) => {
|
||||||
formComponent.settings = formComp.settings;
|
formComponent.settings = formComp.settings;
|
||||||
setFormValid(isValid);
|
setFormValid(isValid);
|
||||||
|
|
@ -507,7 +511,7 @@ export default function ComponentSettings(props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCableComponent = () => {
|
const hasCableID = () => {
|
||||||
return (formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
|
return (formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
|
||||||
formComponent.settings.type ===
|
formComponent.settings.type ===
|
||||||
quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
|
quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
|
||||||
|
|
@ -519,7 +523,9 @@ export default function ComponentSettings(props: Props) {
|
||||||
|
|
||||||
const supportsExpansion = () => {
|
const supportsExpansion = () => {
|
||||||
return ((formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
|
return ((formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
|
||||||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) &&
|
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
|
||||||
|
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE
|
||||||
|
) &&
|
||||||
formComponent.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C)
|
formComponent.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -577,7 +583,8 @@ export default function ComponentSettings(props: Props) {
|
||||||
)}
|
)}
|
||||||
</TextField>
|
</TextField>
|
||||||
{supportsExpansion() && setExpLine()}
|
{supportsExpansion() && setExpLine()}
|
||||||
{(isCableComponent() && !supportsExpansion())&& setComponentAddrType()}
|
|
||||||
|
{(hasCableID() && !supportsExpansion())&& setComponentAddrType()}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : activeStep === 1 ? (
|
) : activeStep === 1 ? (
|
||||||
<ComponentForm
|
<ComponentForm
|
||||||
|
|
@ -867,6 +874,167 @@ export default function ComponentSettings(props: Props) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addControlEmail = () => {
|
||||||
|
const trimmed = controlEmail.trim().toLowerCase();
|
||||||
|
if (trimmed === "" || formComponent.settings.controlEmails.includes(trimmed)) return;
|
||||||
|
let f = cloneDeep(formComponent);
|
||||||
|
f.settings.controlEmails.push(trimmed);
|
||||||
|
setFormComponent(f);
|
||||||
|
setControlEmail("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeControlEmail = (index: number) => {
|
||||||
|
let f = cloneDeep(formComponent);
|
||||||
|
f.settings.controlEmails.splice(index, 1);
|
||||||
|
setFormComponent(f);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOutputModeChanged = (event: any) => {
|
||||||
|
let f = cloneDeep(formComponent);
|
||||||
|
f.settings.defaultOutputState = event.target.value;
|
||||||
|
setFormComponent(f);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMinCycleTimeChanged = (ms: number) => {
|
||||||
|
let f = cloneDeep(formComponent);
|
||||||
|
f.settings.minCycleTimeMs = Number(ms);
|
||||||
|
setFormComponent(f);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMinCycleTimeValid = () => {
|
||||||
|
const ext = extension(formComponent.settings.type, formComponent.settings.subtype);
|
||||||
|
if (!ext.isController) return true;
|
||||||
|
let min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0);
|
||||||
|
return formComponent.settings.minCycleTimeMs >= min;
|
||||||
|
};
|
||||||
|
|
||||||
|
const minCycleTimeDescription = () => {
|
||||||
|
const ext = extension(formComponent.settings.type, formComponent.settings.subtype);
|
||||||
|
if (!ext.isController) return "";
|
||||||
|
const min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0);
|
||||||
|
const unit = bestUnit(min);
|
||||||
|
const value = milliToX(min, unit).toString();
|
||||||
|
return "Must be at least " + value + " " + unit;
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlContent = () => {
|
||||||
|
return (
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
id="output-mode"
|
||||||
|
name="outputMode"
|
||||||
|
select
|
||||||
|
label="Output Mode"
|
||||||
|
value={formComponent.settings.defaultOutputState}
|
||||||
|
onChange={handleOutputModeChanged}
|
||||||
|
margin="normal"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canEdit}>
|
||||||
|
<MenuItem value={0}>Auto</MenuItem>
|
||||||
|
<MenuItem value={1}>On</MenuItem>
|
||||||
|
<MenuItem value={2}>Off</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<PeriodSelect
|
||||||
|
id="minCycleTimeMs"
|
||||||
|
label="Minimum cycle time"
|
||||||
|
units={["seconds", "minutes"]}
|
||||||
|
isDisabled={!canEdit}
|
||||||
|
isError={!isMinCycleTimeValid()}
|
||||||
|
initialMs={
|
||||||
|
formComponent.settings.minCycleTimeMs
|
||||||
|
? formComponent.settings.minCycleTimeMs
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={handleMinCycleTimeChanged}
|
||||||
|
helperText={
|
||||||
|
isMinCycleTimeValid()
|
||||||
|
? "The minimum amount of time that the component spends in a state"
|
||||||
|
: minCycleTimeDescription()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Typography variant="subtitle2" sx={{ marginTop: 2, marginBottom: 1 }}>
|
||||||
|
Authorized Control Emails
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={1} alignItems="center">
|
||||||
|
<Grid size={{ xs: 9 }}>
|
||||||
|
<TextField
|
||||||
|
label="Email address"
|
||||||
|
type="email"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
value={controlEmail}
|
||||||
|
onChange={e => setControlEmail(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
addControlEmail();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!canEdit}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 3 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
onClick={addControlEmail}
|
||||||
|
disabled={!canEdit || controlEmail.trim() === ""}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
{formComponent.settings.controlEmails.map((email, i) => (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
key={i}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
style={{ marginTop: 4 }}>
|
||||||
|
<Grid>
|
||||||
|
<Typography variant="body2">{email}</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => removeControlEmail(i)}
|
||||||
|
disabled={!canEdit}
|
||||||
|
className={classNames(classes.redButton)}>
|
||||||
|
<RemoveIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
<Divider sx={{ marginTop: 3, marginBottom: 2 }} />
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Email Control
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||||
|
Authorized emails can control this component by sending an email to the
|
||||||
|
control address. Contact your administrator for the control email
|
||||||
|
address. Use one of the following as the email subject:
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
borderRadius: 1,
|
||||||
|
padding: 1.5,
|
||||||
|
marginTop: 1,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: "0.85rem"
|
||||||
|
}}>
|
||||||
|
<div>{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - ON</div>
|
||||||
|
<div>{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - OFF</div>
|
||||||
|
<div>{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - AUTO</div>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const actions = () => {
|
const actions = () => {
|
||||||
return (
|
return (
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|
@ -1042,7 +1210,25 @@ export default function ComponentSettings(props: Props) {
|
||||||
scroll="paper">
|
scroll="paper">
|
||||||
{title()}
|
{title()}
|
||||||
<Divider />
|
<Divider />
|
||||||
{!isController(formComponent.settings.type) && (
|
{isController(formComponent.settings.type) ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tabs
|
||||||
|
value={tabVal}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
onChange={handleChange}>
|
||||||
|
<Tab label="General" />
|
||||||
|
<Tab label="Control" />
|
||||||
|
</Tabs>
|
||||||
|
<TabPanelMine value={tabVal} index={0}>
|
||||||
|
{content()}
|
||||||
|
</TabPanelMine>
|
||||||
|
<TabPanelMine value={tabVal} index={1}>
|
||||||
|
{controlContent()}
|
||||||
|
</TabPanelMine>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tabVal}
|
value={tabVal}
|
||||||
indicatorColor="primary"
|
indicatorColor="primary"
|
||||||
|
|
@ -1052,7 +1238,6 @@ export default function ComponentSettings(props: Props) {
|
||||||
<Tab label="Overlays" />
|
<Tab label="Overlays" />
|
||||||
<Tab label="Nodes" disabled={!prevComponent} />
|
<Tab label="Nodes" disabled={!prevComponent} />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
|
||||||
<TabPanelMine value={tabVal} index={0}>
|
<TabPanelMine value={tabVal} index={0}>
|
||||||
{content()}
|
{content()}
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
|
|
@ -1072,6 +1257,8 @@ export default function ComponentSettings(props: Props) {
|
||||||
<TabPanelMine value={tabVal} index={2}>
|
<TabPanelMine value={tabVal} index={2}>
|
||||||
{componentPrefs()}
|
{componentPrefs()}
|
||||||
</TabPanelMine>
|
</TabPanelMine>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
{actions()}
|
{actions()}
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/material";
|
import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/material";
|
||||||
import { DataUsage, SimCard, Launch, DeviceHub } from "@mui/icons-material";
|
import { DataUsage, SimCard, Launch, DeviceHub } from "@mui/icons-material";
|
||||||
import StatusChip from "common/StatusChip";
|
import PendingChangesChip from "common/PendingChangesChip";
|
||||||
import DeviceTags from "device/DeviceTags";
|
import DeviceTags from "device/DeviceTags";
|
||||||
import VersionChip from "device/VersionChip";
|
import VersionChip from "device/VersionChip";
|
||||||
import { useSnackbar, useWidth, usePrevious } from "hooks";
|
import { useSnackbar, useWidth, usePendingChanges, usePrevious } from "hooks";
|
||||||
import { Device, latestFirmwareVersion, Component } from "models";
|
import { Device, latestFirmwareVersion, Component } from "models";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { describeConnectivity } from "pbHelpers/Connectivity";
|
import { describeConnectivity } from "pbHelpers/Connectivity";
|
||||||
|
|
@ -54,6 +54,10 @@ export default function DeviceOverview(props: Props) {
|
||||||
// const match = useRouteMatch<MatchParams>();
|
// const match = useRouteMatch<MatchParams>();
|
||||||
const [powerComponent, setPowerComponent] = useState<Component | undefined | null>();
|
const [powerComponent, setPowerComponent] = useState<Component | undefined | null>();
|
||||||
const [modemComponent, setModemComponent] = useState<Component | undefined | null>();
|
const [modemComponent, setModemComponent] = useState<Component | undefined | null>();
|
||||||
|
const { pending: pendingChanges, accepted: changesAccepted } = usePendingChanges(
|
||||||
|
device.status.synced,
|
||||||
|
!loading && device.id() > 0
|
||||||
|
);
|
||||||
// const [hologramDialog, setHologramDialog] = useState(false);
|
// const [hologramDialog, setHologramDialog] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -228,9 +232,9 @@ export default function DeviceOverview(props: Props) {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{!device.status.synced && (
|
{(pendingChanges || changesAccepted) && (
|
||||||
<Grid>
|
<Grid>
|
||||||
<StatusChip status="pending" />
|
<PendingChangesChip pending={pendingChanges} accepted={changesAccepted} />
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
{device.settings.needsBusControl && (
|
{device.settings.needsBusControl && (
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import LinearMutationBuilder from "common/LinearMutationBuilder";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import CancelSubmit from "common/CancelSubmit";
|
import CancelSubmit from "common/CancelSubmit";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -94,7 +95,7 @@ export default function DeviceSettings(props: Props) {
|
||||||
const { success, error } = useSnackbar();
|
const { success, error } = useSnackbar();
|
||||||
const deviceAPI = useDeviceAPI();
|
const deviceAPI = useDeviceAPI();
|
||||||
const { device, isDialogOpen, closeDialogCallback, canEdit, refreshCallback, components } = props;
|
const { device, isDialogOpen, closeDialogCallback, canEdit, refreshCallback, components } = props;
|
||||||
const prevDevice = usePrevious(device);
|
const prevIsDialogOpen = usePrevious(isDialogOpen);
|
||||||
const [deviceForm, setDeviceForm] = useState<Device>(deviceFromForm(Device.clone(device)));
|
const [deviceForm, setDeviceForm] = useState<Device>(deviceFromForm(Device.clone(device)));
|
||||||
const [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState<boolean>(false);
|
const [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState<boolean>(false);
|
||||||
const [sleeps, setSleeps] = useState<boolean>(device.settings.sleepDurationS > 0);
|
const [sleeps, setSleeps] = useState<boolean>(device.settings.sleepDurationS > 0);
|
||||||
|
|
@ -102,7 +103,9 @@ export default function DeviceSettings(props: Props) {
|
||||||
const [compExtTwo, setCompExtTwo] = useState("");
|
const [compExtTwo, setCompExtTwo] = useState("");
|
||||||
const [compExtThree, setCompExtThree] = useState("");
|
const [compExtThree, setCompExtThree] = useState("");
|
||||||
const [currentTab, setCurrentTab] = useState(0);
|
const [currentTab, setCurrentTab] = useState(0);
|
||||||
const [linearMutations, setLinearMutations] = useState<pond.LinearMutation[]>([]);
|
const [linearMutations, setLinearMutations] = useState<pond.LinearMutation[]>(
|
||||||
|
cloneDeep(device.settings.mutations)
|
||||||
|
);
|
||||||
const [componentsByDevice, setComponentsByDevice] = useState<Map<string, Component[]>>(
|
const [componentsByDevice, setComponentsByDevice] = useState<Map<string, Component[]>>(
|
||||||
new Map<string, Component[]>()
|
new Map<string, Component[]>()
|
||||||
);
|
);
|
||||||
|
|
@ -110,27 +113,22 @@ export default function DeviceSettings(props: Props) {
|
||||||
const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>();
|
const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevDevice !== device) {
|
if (!isDialogOpen || prevIsDialogOpen) return;
|
||||||
setDeviceForm(deviceFromForm(Device.clone(props.device)));
|
|
||||||
if (device.settings.extensionComponents[0]) {
|
const settingsForm = deviceFromForm(Device.clone(device));
|
||||||
setCompExtOne(device.settings.extensionComponents[0]);
|
setDeviceForm(settingsForm);
|
||||||
}
|
setSleeps(settingsForm.settings.sleepDurationS > 0);
|
||||||
if (device.settings.extensionComponents[1]) {
|
setCompExtOne(settingsForm.settings.extensionComponents[0] ?? "");
|
||||||
setCompExtTwo(device.settings.extensionComponents[1]);
|
setCompExtTwo(settingsForm.settings.extensionComponents[1] ?? "");
|
||||||
}
|
setCompExtThree(settingsForm.settings.extensionComponents[2] ?? "");
|
||||||
if (device.settings.extensionComponents[2]) {
|
setLinearMutations(cloneDeep(settingsForm.settings.mutations));
|
||||||
setCompExtThree(device.settings.extensionComponents[2]);
|
|
||||||
}
|
|
||||||
if (device.settings.mutations) {
|
|
||||||
setLinearMutations(device.settings.mutations);
|
|
||||||
}
|
|
||||||
if (components) {
|
if (components) {
|
||||||
let cbd = new Map<string, Component[]>();
|
let cbd = new Map<string, Component[]>();
|
||||||
cbd.set(device.id().toString(), components);
|
cbd.set(settingsForm.id().toString(), components);
|
||||||
setComponentsByDevice(cbd);
|
setComponentsByDevice(cbd);
|
||||||
}
|
}
|
||||||
}
|
}, [device, components, isDialogOpen, prevIsDialogOpen]);
|
||||||
}, [device, prevDevice, props.device, components]);
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
closeDialogCallback();
|
closeDialogCallback();
|
||||||
|
|
@ -145,7 +143,7 @@ export default function DeviceSettings(props: Props) {
|
||||||
|
|
||||||
const minCheckPeriodS = (): number => {
|
const minCheckPeriodS = (): number => {
|
||||||
let defaultPeriod = 60;
|
let defaultPeriod = 60;
|
||||||
switch (device.settings.platform) {
|
switch (deviceForm.settings.platform) {
|
||||||
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
|
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
|
||||||
return user.hasFeature("admin") ? defaultPeriod : 300;
|
return user.hasFeature("admin") ? defaultPeriod : 300;
|
||||||
default:
|
default:
|
||||||
|
|
@ -317,7 +315,7 @@ export default function DeviceSettings(props: Props) {
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
defaultValue={deviceForm.settings.name}
|
value={deviceForm.settings.name}
|
||||||
onChange={changeName}
|
onChange={changeName}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|
@ -330,7 +328,7 @@ export default function DeviceSettings(props: Props) {
|
||||||
id="description"
|
id="description"
|
||||||
name="description"
|
name="description"
|
||||||
label="Description"
|
label="Description"
|
||||||
defaultValue={deviceForm.settings.description}
|
value={deviceForm.settings.description}
|
||||||
onChange={changeDescription}
|
onChange={changeDescription}
|
||||||
multiline
|
multiline
|
||||||
rows={2}
|
rows={2}
|
||||||
|
|
@ -548,8 +546,8 @@ export default function DeviceSettings(props: Props) {
|
||||||
|
|
||||||
const mutationsContent = () => {
|
const mutationsContent = () => {
|
||||||
let mutations: JSX.Element[] = [];
|
let mutations: JSX.Element[] = [];
|
||||||
if (device.settings.mutations) {
|
if (linearMutations) {
|
||||||
device.settings.mutations.forEach((mut, i) => {
|
linearMutations.forEach((mut, i) => {
|
||||||
mutations.push(
|
mutations.push(
|
||||||
<ListItem key={"mutation" + i}>
|
<ListItem key={"mutation" + i}>
|
||||||
<Grid
|
<Grid
|
||||||
|
|
@ -566,9 +564,7 @@ export default function DeviceSettings(props: Props) {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
style={{ backgroundColor: "red" }}
|
style={{ backgroundColor: "red" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
let lm = linearMutations;
|
setLinearMutations(linearMutations.filter((_, index) => index !== i));
|
||||||
lm.splice(i, 1);
|
|
||||||
setLinearMutations([...lm]);
|
|
||||||
}}>
|
}}>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -694,9 +690,7 @@ export default function DeviceSettings(props: Props) {
|
||||||
setNewMutationDialog(false);
|
setNewMutationDialog(false);
|
||||||
}}
|
}}
|
||||||
onSubmit={newMutation => {
|
onSubmit={newMutation => {
|
||||||
let lm = linearMutations;
|
setLinearMutations([...linearMutations, newMutation]);
|
||||||
lm.push(newMutation);
|
|
||||||
setLinearMutations(lm);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,14 @@ class VersionChip extends React.Component<Props, State> {
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
label={firmwareVersionHelper.description}
|
label={firmwareVersionHelper.description}
|
||||||
icon={firmwareVersionHelper.icon}
|
icon={firmwareVersionHelper.icon}
|
||||||
|
sx={
|
||||||
|
firmwareVersionHelper.colour
|
||||||
|
? {
|
||||||
|
borderColor: firmwareVersionHelper.colour,
|
||||||
|
color: firmwareVersionHelper.colour
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,6 @@ export default function DeviceScannedComponents(props: Props){
|
||||||
let expanders: quack.AddressData[] = []
|
let expanders: quack.AddressData[] = []
|
||||||
//filter the address data
|
//filter the address data
|
||||||
let addr = scannedI2C?.settings?.foundAddresses
|
let addr = scannedI2C?.settings?.foundAddresses
|
||||||
console.log(addr)
|
|
||||||
if(addr){
|
if(addr){
|
||||||
addr.forEach(addrData => {
|
addr.forEach(addrData => {
|
||||||
if(!i2cBlacklistAddresses.includes(addrData.address)){
|
if(!i2cBlacklistAddresses.includes(addrData.address)){
|
||||||
|
|
@ -87,7 +86,6 @@ export default function DeviceScannedComponents(props: Props){
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log(valid)
|
|
||||||
setExpanderAddresses(expanders)
|
setExpanderAddresses(expanders)
|
||||||
setValidI2CComponentAddresses(valid)
|
setValidI2CComponentAddresses(valid)
|
||||||
},[scannedI2C])
|
},[scannedI2C])
|
||||||
|
|
@ -213,14 +211,13 @@ export default function DeviceScannedComponents(props: Props){
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log(cloneList)
|
|
||||||
setComponents(cloneList)
|
setComponents(cloneList)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeScan = (key: string, type: pond.ObjectType) => {
|
const removeScan = (key: string, type: pond.ObjectType) => {
|
||||||
deviceAPI.removeFoundComponents(device.settings.deviceId, key, type)
|
deviceAPI.removeFoundComponents(device.settings.deviceId, key, type)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
console.log("Cleared this scan")
|
// console.log("Cleared this scan")
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log("There was a problem clearing this scan")
|
console.log("There was a problem clearing this scan")
|
||||||
})
|
})
|
||||||
|
|
@ -229,7 +226,7 @@ export default function DeviceScannedComponents(props: Props){
|
||||||
const removeAllScans = () => {
|
const removeAllScans = () => {
|
||||||
deviceAPI.removeAllFoundComponents(device.settings.deviceId)
|
deviceAPI.removeAllFoundComponents(device.settings.deviceId)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
console.log("Cleared all scans")
|
// console.log("Cleared all scans")
|
||||||
refreshCallback()
|
refreshCallback()
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.log("There was a problem clearing scans")
|
console.log("There was a problem clearing scans")
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export {
|
||||||
export * from "./useDebounce";
|
export * from "./useDebounce";
|
||||||
// export * from "./useForceUpdate";
|
// export * from "./useForceUpdate";
|
||||||
// export * from "./useInterval";
|
// export * from "./useInterval";
|
||||||
|
export * from "./usePendingChanges";
|
||||||
export * from "./usePrevious";
|
export * from "./usePrevious";
|
||||||
export * from "./useThemeType";
|
export * from "./useThemeType";
|
||||||
export * from "./useWidth";
|
export * from "./useWidth";
|
||||||
|
|
|
||||||
31
src/hooks/usePendingChanges.ts
Normal file
31
src/hooks/usePendingChanges.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks a synced flag and reports whether changes are pending or were
|
||||||
|
* accepted while mounted (a false -> true transition, e.g. from a websocket
|
||||||
|
* status update). Pass enabled=false until real data has loaded so the
|
||||||
|
* initial placeholder -> loaded flip is not mistaken for an accepted change.
|
||||||
|
*/
|
||||||
|
export function usePendingChanges(synced: boolean, enabled: boolean = true) {
|
||||||
|
const prevSynced = useRef<boolean | undefined>(undefined);
|
||||||
|
const [accepted, setAccepted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
prevSynced.current = undefined;
|
||||||
|
setAccepted(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prevSynced.current === false && synced) {
|
||||||
|
setAccepted(true);
|
||||||
|
} else if (!synced) {
|
||||||
|
setAccepted(false);
|
||||||
|
}
|
||||||
|
prevSynced.current = synced;
|
||||||
|
}, [synced, enabled]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pending: enabled && !synced,
|
||||||
|
accepted: enabled && synced && accepted
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -32,7 +32,6 @@ import moment from "moment-timezone";
|
||||||
import {
|
import {
|
||||||
componentIDToString,
|
componentIDToString,
|
||||||
emptyComponentId,
|
emptyComponentId,
|
||||||
getComponentIDString,
|
|
||||||
sameComponentID,
|
sameComponentID,
|
||||||
stringToComponentId
|
stringToComponentId
|
||||||
} from "pbHelpers/Component";
|
} from "pbHelpers/Component";
|
||||||
|
|
@ -130,9 +129,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { success, error } = useSnackbar();
|
const { success, error } = useSnackbar();
|
||||||
const prevInitialInteraction = usePrevious(initialInteraction);
|
const prevIsDialogOpen = usePrevious(isDialogOpen);
|
||||||
const prevComponents = usePrevious(components);
|
|
||||||
const prevInitialComponent = usePrevious(initialComponent);
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [{ user, as }] = useGlobalState();
|
const [{ user, as }] = useGlobalState();
|
||||||
const interactionsAPI = useInteractionsAPI();
|
const interactionsAPI = useInteractionsAPI();
|
||||||
|
|
@ -166,7 +163,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
const setDefaultState = useCallback(() => {
|
const setDefaultState = useCallback(() => {
|
||||||
let interaction = getDefaultInteraction();
|
let interaction = getDefaultInteraction();
|
||||||
if (initialInteraction && mode === "update") {
|
if (initialInteraction && mode === "update") {
|
||||||
interaction = initialInteraction;
|
interaction = Interaction.clone(initialInteraction);
|
||||||
if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) {
|
if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) {
|
||||||
setSubtypeDropdown(interaction.settings.subtype);
|
setSubtypeDropdown(interaction.settings.subtype);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -222,7 +219,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
setDutyCycle(interactionResult.dutyCycle.toString());
|
setDutyCycle(interactionResult.dutyCycle.toString());
|
||||||
setMappedComponents(mappedComponents);
|
setMappedComponents(mappedComponents);
|
||||||
setInteraction(interaction);
|
setInteraction(interaction);
|
||||||
}, [components, initialComponent, initialConditions, initialInteraction, mode, /*sensor*/]);
|
}, [components, initialComponent, initialConditions, initialInteraction, mode, sensor, user]);
|
||||||
|
|
||||||
const availableSources = () => {
|
const availableSources = () => {
|
||||||
return components.filter(component => isSource(component.settings.type));
|
return components.filter(component => isSource(component.settings.type));
|
||||||
|
|
@ -232,23 +229,12 @@ export default function InteractionSettings(props: Props) {
|
||||||
if (user && user.settings.timezone) {
|
if (user && user.settings.timezone) {
|
||||||
setTimezone(user.settings.timezone);
|
setTimezone(user.settings.timezone);
|
||||||
}
|
}
|
||||||
if (
|
}, [user]);
|
||||||
prevInitialInteraction !== initialInteraction ||
|
|
||||||
(prevComponents && prevComponents.length !== components.length) ||
|
useEffect(() => {
|
||||||
getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent)
|
if (!isDialogOpen || prevIsDialogOpen) return;
|
||||||
) {
|
|
||||||
setDefaultState();
|
setDefaultState();
|
||||||
}
|
}, [isDialogOpen, prevIsDialogOpen, setDefaultState]);
|
||||||
}, [
|
|
||||||
components.length,
|
|
||||||
initialComponent,
|
|
||||||
initialInteraction,
|
|
||||||
prevComponents,
|
|
||||||
prevInitialComponent,
|
|
||||||
prevInitialInteraction,
|
|
||||||
setDefaultState,
|
|
||||||
user
|
|
||||||
]);
|
|
||||||
|
|
||||||
const getAvailableSinks = () => {
|
const getAvailableSinks = () => {
|
||||||
let type = or(interaction.settings.result, pond.InteractionResult.create()).type;
|
let type = or(interaction.settings.result, pond.InteractionResult.create()).type;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Typography
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Settings } from "@mui/icons-material";
|
import { Settings } from "@mui/icons-material";
|
||||||
|
import PendingChangesIndicator from "common/PendingChangesIndicator";
|
||||||
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
|
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
import { Component, Device, Interaction } from "models";
|
import { Component, Device, Interaction } from "models";
|
||||||
|
|
@ -80,6 +81,7 @@ export default function InteractionsOverview(props: Props) {
|
||||||
const [interactions, setInteractions] = useState<Interaction[]>(props.interactions);
|
const [interactions, setInteractions] = useState<Interaction[]>(props.interactions);
|
||||||
const [dirtyInteractions, setDirtyInteractions] = useState<Map<number, boolean>>(new Map());
|
const [dirtyInteractions, setDirtyInteractions] = useState<Map<number, boolean>>(new Map());
|
||||||
const [mappedComponents, setMappedComponents] = useState<Map<string, Component>>(new Map());
|
const [mappedComponents, setMappedComponents] = useState<Map<string, Component>>(new Map());
|
||||||
|
const [acceptedInteractions, setAcceptedInteractions] = useState<Set<string>>(new Set());
|
||||||
const [renderToggle, setRenderToggle] = useState(false);
|
const [renderToggle, setRenderToggle] = useState(false);
|
||||||
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | undefined>(
|
const [selectedInteraction, setSelectedInteraction] = useState<Interaction | undefined>(
|
||||||
undefined
|
undefined
|
||||||
|
|
@ -95,6 +97,19 @@ export default function InteractionsOverview(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.interactions !== prevInteractions) {
|
if (props.interactions !== prevInteractions) {
|
||||||
|
setAcceptedInteractions(prev => {
|
||||||
|
const updated = new Set(prev);
|
||||||
|
props.interactions.forEach((interaction: Interaction) => {
|
||||||
|
const key = interaction.key();
|
||||||
|
const prevInteraction = prevInteractions?.find(p => p.key() === key);
|
||||||
|
if (!interaction.status.synced) {
|
||||||
|
updated.delete(key);
|
||||||
|
} else if (prevInteraction && !prevInteraction.status.synced) {
|
||||||
|
updated.add(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
setInteractions(cloneDeep(props.interactions));
|
setInteractions(cloneDeep(props.interactions));
|
||||||
}
|
}
|
||||||
}, [components, prevComponents, props.interactions, prevInteractions]);
|
}, [components, prevComponents, props.interactions, prevInteractions]);
|
||||||
|
|
@ -207,8 +222,23 @@ export default function InteractionsOverview(props: Props) {
|
||||||
let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
|
let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
|
||||||
updatedDirtyInteractions.set(index, false);
|
updatedDirtyInteractions.set(index, false);
|
||||||
setDirtyInteractions(updatedDirtyInteractions);
|
setDirtyInteractions(updatedDirtyInteractions);
|
||||||
|
setInteractions(prev => {
|
||||||
|
const updated = cloneDeep(prev);
|
||||||
|
if (updated[index]) {
|
||||||
|
updated[index].status.synced = false;
|
||||||
|
updated[index].status.lastUpdate = moment().toISOString();
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
const interactionKey = settings.key ?? "";
|
||||||
|
if (interactionKey) {
|
||||||
|
setAcceptedInteractions(prev => {
|
||||||
|
const updated = new Set(prev);
|
||||||
|
updated.delete(interactionKey);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
success("Successfully updated the interaction for " + component.name());
|
success("Successfully updated the interaction for " + component.name());
|
||||||
refreshCallback();
|
|
||||||
})
|
})
|
||||||
.catch((_err: any) => {
|
.catch((_err: any) => {
|
||||||
error("Error occurred while updating the interaction for " + component.name());
|
error("Error occurred while updating the interaction for " + component.name());
|
||||||
|
|
@ -226,10 +256,12 @@ export default function InteractionsOverview(props: Props) {
|
||||||
let key = interaction.key();
|
let key = interaction.key();
|
||||||
let source = mappedComponents.get(componentIDToString(interaction.settings.source));
|
let source = mappedComponents.get(componentIDToString(interaction.settings.source));
|
||||||
let sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
|
let sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
|
||||||
let statusText =
|
const isAccepted = acceptedInteractions.has(interaction.key());
|
||||||
!interaction.status.synced && interaction.status.lastUpdate
|
const isPending = !interaction.status.synced;
|
||||||
? "Pending " +
|
let statusText = isAccepted
|
||||||
moment(interaction.status.lastUpdate && interaction.status.lastUpdate).fromNow()
|
? "Pending change accepted"
|
||||||
|
: isPending
|
||||||
|
? "Pending " + moment(interaction.status.lastUpdate).fromNow()
|
||||||
: "";
|
: "";
|
||||||
let schedule = pond.InteractionSchedule.create(
|
let schedule = pond.InteractionSchedule.create(
|
||||||
interaction.settings.schedule !== null ? interaction.settings.schedule : undefined
|
interaction.settings.schedule !== null ? interaction.settings.schedule : undefined
|
||||||
|
|
@ -272,7 +304,14 @@ export default function InteractionsOverview(props: Props) {
|
||||||
className={classes.header}
|
className={classes.header}
|
||||||
titleTypographyProps={{ variant: "subtitle2" }}
|
titleTypographyProps={{ variant: "subtitle2" }}
|
||||||
title={interactionResultText(interaction, sink)}
|
title={interactionResultText(interaction, sink)}
|
||||||
subheader={statusText}
|
subheader={
|
||||||
|
statusText ? (
|
||||||
|
<PendingChangesIndicator
|
||||||
|
pending={isPending}
|
||||||
|
accepted={isAccepted}
|
||||||
|
text={statusText}
|
||||||
|
/>
|
||||||
|
) : ""}
|
||||||
subheaderTypographyProps={{ variant: "caption" }}
|
subheaderTypographyProps={{ variant: "caption" }}
|
||||||
action={action}
|
action={action}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap }
|
||||||
import ComponentCard from "component/ComponentCard";
|
import ComponentCard from "component/ComponentCard";
|
||||||
import { sameComponentID, sortComponents } from "pbHelpers/Component";
|
import { sameComponentID, sortComponents } from "pbHelpers/Component";
|
||||||
import { isController } from "pbHelpers/ComponentType";
|
import { isController } from "pbHelpers/ComponentType";
|
||||||
import { or } from "utils";
|
import { getTimestampMillis, isStaleStatusUpdate, or } from "utils";
|
||||||
import ComponentDiagnostics from "component/ComponentDiagnostics";
|
import ComponentDiagnostics from "component/ComponentDiagnostics";
|
||||||
import DeviceWizard from "device/DeviceWizard";
|
import DeviceWizard from "device/DeviceWizard";
|
||||||
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
|
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
|
||||||
|
|
@ -27,12 +27,6 @@ import { useWebSocket } from "hooks/useWebSocket";
|
||||||
|
|
||||||
type TimestampedReading = { timestamp?: string };
|
type TimestampedReading = { timestamp?: string };
|
||||||
|
|
||||||
function getTimestampMillis(timestamp?: string): number | undefined {
|
|
||||||
if (!timestamp) return undefined;
|
|
||||||
const parsed = Date.parse(timestamp);
|
|
||||||
return Number.isNaN(parsed) ? undefined : parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickNewestReading<T extends TimestampedReading>(
|
function pickNewestReading<T extends TimestampedReading>(
|
||||||
current: T | null | undefined,
|
current: T | null | undefined,
|
||||||
incoming: T | null | undefined
|
incoming: T | null | undefined
|
||||||
|
|
@ -270,6 +264,10 @@ export default function DevicePage() {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const { key, component } = data;
|
const { key, component } = data;
|
||||||
setComponents((prev) => {
|
setComponents((prev) => {
|
||||||
|
const existing = prev.get(key);
|
||||||
|
if (existing && isStaleStatusUpdate(existing.status, component.status)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
const updated = new Map(prev);
|
const updated = new Map(prev);
|
||||||
updated.set(key, component);
|
updated.set(key, component);
|
||||||
return updated;
|
return updated;
|
||||||
|
|
@ -282,6 +280,54 @@ export default function DevicePage() {
|
||||||
enabled: !!deviceID,
|
enabled: !!deviceID,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Real-time interaction updates ---
|
||||||
|
// Streams interaction changes for this device, including status changes
|
||||||
|
// when the device accepts pending interaction updates.
|
||||||
|
useWebSocket<{ key: string; interaction: Interaction } | null>({
|
||||||
|
path: `/v1/live/devices/${deviceID}/interactions`,
|
||||||
|
parse: (e) => {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(e.data);
|
||||||
|
const interaction = Interaction.any(raw);
|
||||||
|
if (!interaction?.settings) {
|
||||||
|
console.warn("[ws] received interaction without settings:", raw);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { key: interaction.key(), interaction };
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[ws] failed to parse interaction:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMessage: (data) => {
|
||||||
|
if (!data) return;
|
||||||
|
const { key, interaction } = data;
|
||||||
|
console.log("[ws:interaction] received", key, {
|
||||||
|
synced: interaction.status.synced,
|
||||||
|
lastUpdate: interaction.status.lastUpdate,
|
||||||
|
lastSynced: interaction.status.lastSynced,
|
||||||
|
});
|
||||||
|
setInteractions((prev) => {
|
||||||
|
const index = prev.findIndex((existing) => existing.key() === key);
|
||||||
|
if (index < 0) {
|
||||||
|
return [...prev, interaction];
|
||||||
|
}
|
||||||
|
const existing = prev[index];
|
||||||
|
if (isStaleStatusUpdate(existing.status, interaction.status)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[index] = interaction;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
keys: liveContextKeys,
|
||||||
|
types: liveContextTypes,
|
||||||
|
as: liveAs,
|
||||||
|
token,
|
||||||
|
enabled: !!deviceID,
|
||||||
|
});
|
||||||
|
|
||||||
const loadPortScan = () => {
|
const loadPortScan = () => {
|
||||||
deviceAPI.listFoundComponents(parseInt(deviceID))
|
deviceAPI.listFoundComponents(parseInt(deviceID))
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
|
|
@ -395,7 +441,7 @@ export default function DevicePage() {
|
||||||
interactions={filteredInteractions}
|
interactions={filteredInteractions}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
deviceComponentPreferences={prefsMap.get(c.key())}
|
deviceComponentPreferences={prefsMap.get(c.key())}
|
||||||
key={i}
|
key={c.key()}
|
||||||
refreshCallback={(updatedComponent?: Component) =>
|
refreshCallback={(updatedComponent?: Component) =>
|
||||||
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
||||||
}
|
}
|
||||||
|
|
@ -412,7 +458,7 @@ export default function DevicePage() {
|
||||||
interactions={filteredInteractions}
|
interactions={filteredInteractions}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
deviceComponentPreferences={prefsMap.get(c.key())}
|
deviceComponentPreferences={prefsMap.get(c.key())}
|
||||||
key={i}
|
key={c.key()}
|
||||||
refreshCallback={(updatedComponent?: Component) =>
|
refreshCallback={(updatedComponent?: Component) =>
|
||||||
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { NextMeasurementChip } from "common/NextMeasurementChip";
|
||||||
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
||||||
// import { getTableIcons } from "common/ResponsiveTable";
|
// import { getTableIcons } from "common/ResponsiveTable";
|
||||||
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
import SmartBreadcrumb from "common/SmartBreadcrumb";
|
||||||
import StatusChip from "common/StatusChip";
|
import PendingChangesChip from "common/PendingChangesChip";
|
||||||
import { GetDefaultDateRange } from "common/time/DateRange";
|
import { GetDefaultDateRange } from "common/time/DateRange";
|
||||||
import ComponentActions from "component/ComponentActions";
|
import ComponentActions from "component/ComponentActions";
|
||||||
import ComponentChart from "component/ComponentChart";
|
import ComponentChart from "component/ComponentChart";
|
||||||
|
|
@ -16,11 +16,14 @@ import {
|
||||||
useComponentAPI,
|
useComponentAPI,
|
||||||
useDeviceAPI,
|
useDeviceAPI,
|
||||||
useGroupAPI,
|
useGroupAPI,
|
||||||
|
useHTTP,
|
||||||
useInteractionsAPI,
|
useInteractionsAPI,
|
||||||
|
usePendingChanges,
|
||||||
usePrevious,
|
usePrevious,
|
||||||
useSnackbar,
|
useSnackbar,
|
||||||
useUserAPI
|
useUserAPI
|
||||||
} from "hooks";
|
} from "hooks";
|
||||||
|
import { useWebSocket } from "hooks/useWebSocket";
|
||||||
import InteractionChip from "interactions/InteractionChip";
|
import InteractionChip from "interactions/InteractionChip";
|
||||||
import InteractionSettings from "interactions/InteractionSettings";
|
import InteractionSettings from "interactions/InteractionSettings";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
|
|
@ -40,9 +43,10 @@ import { getDefaultInteraction } from "pbHelpers/Interaction";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { useGlobalState } from "providers";
|
import { useGlobalState } from "providers";
|
||||||
import { useTeamAPI } from "providers/pond/teamAPI";
|
import { useTeamAPI } from "providers/pond/teamAPI";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
// import { useRouteMatch } from "react-router";
|
// import { useRouteMatch } from "react-router";
|
||||||
|
import { isStaleStatusUpdate } from "utils/syncStatus";
|
||||||
import { or } from "utils/types";
|
import { or } from "utils/types";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => {
|
const useStyles = makeStyles((theme: Theme) => {
|
||||||
|
|
@ -142,6 +146,55 @@ export default function DeviceComponent() {
|
||||||
const [deviceComponentPrefs, setDeviceComponentPrefs] = useState<
|
const [deviceComponentPrefs, setDeviceComponentPrefs] = useState<
|
||||||
pond.DeviceComponentPreferences
|
pond.DeviceComponentPreferences
|
||||||
>();
|
>();
|
||||||
|
const { token } = useHTTP();
|
||||||
|
const componentPending = usePendingChanges(
|
||||||
|
component.status.synced,
|
||||||
|
!loadingInitial && component.key() !== ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const liveContextKeys = useMemo(() => getContextKeys(), []);
|
||||||
|
const liveContextTypes = useMemo(() => getContextTypes(), []);
|
||||||
|
/** Matches the REST calls: omit ?as= when first context type is "team". */
|
||||||
|
const liveAs =
|
||||||
|
as && !(liveContextTypes.length > 0 && liveContextTypes[0] === "team")
|
||||||
|
? as
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// --- Real-time component updates ---
|
||||||
|
// Streams component changes for this device, including status changes
|
||||||
|
// when the device accepts pending component updates.
|
||||||
|
useWebSocket<{ key: string; component: Component } | null>({
|
||||||
|
path: `/v1/live/devices/${deviceID}/components`,
|
||||||
|
parse: (e) => {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(e.data);
|
||||||
|
const comp = Component.any(raw);
|
||||||
|
if (!comp?.settings) {
|
||||||
|
console.warn("[ws] received component without settings:", raw);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { key: comp.key(), component: comp };
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[ws] failed to parse component:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMessage: (data) => {
|
||||||
|
if (!data || data.key !== componentID) return;
|
||||||
|
setComponent((prev) => {
|
||||||
|
if (prev.key() !== data.key) return prev;
|
||||||
|
if (isStaleStatusUpdate(prev.status, data.component.status)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return data.component;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
keys: liveContextKeys,
|
||||||
|
types: liveContextTypes,
|
||||||
|
as: liveAs,
|
||||||
|
token,
|
||||||
|
enabled: !!deviceID && componentID !== "",
|
||||||
|
});
|
||||||
|
|
||||||
const setDefaultState = () => {
|
const setDefaultState = () => {
|
||||||
setDevice(Device.create());
|
setDevice(Device.create());
|
||||||
|
|
@ -389,9 +442,12 @@ export default function DeviceComponent() {
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
wrap="nowrap"
|
wrap="nowrap"
|
||||||
className={classes.overviewContainer}>
|
className={classes.overviewContainer}>
|
||||||
{!component.status.synced && (
|
{(componentPending.pending || componentPending.accepted) && (
|
||||||
<Grid >
|
<Grid >
|
||||||
<StatusChip key="status" status="pending" />
|
<PendingChangesChip
|
||||||
|
pending={componentPending.pending}
|
||||||
|
accepted={componentPending.accepted}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
<Grid >
|
<Grid >
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ import {
|
||||||
Voltage,
|
Voltage,
|
||||||
VPD,
|
VPD,
|
||||||
Weight,
|
Weight,
|
||||||
CapacitorCable
|
CapacitorCable,
|
||||||
|
AnalogPressure
|
||||||
} from "pbHelpers/ComponentTypes";
|
} from "pbHelpers/ComponentTypes";
|
||||||
//import { multilineGrainCableData } from "pbHelpers/ComponentTypes/GrainCable";
|
//import { multilineGrainCableData } from "pbHelpers/ComponentTypes/GrainCable";
|
||||||
//import { multilineCapCableData } from "./ComponentTypes/CapacitorCable";
|
//import { multilineCapCableData } from "./ComponentTypes/CapacitorCable";
|
||||||
|
|
@ -96,7 +97,8 @@ const COMPONENT_TYPE_MAP = new Map<quack.ComponentType, Function>([
|
||||||
[quack.ComponentType.COMPONENT_TYPE_SEN5X, Sen5x],
|
[quack.ComponentType.COMPONENT_TYPE_SEN5X, Sen5x],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_VIBRATION_CHAIN, VibrationCable],
|
[quack.ComponentType.COMPONENT_TYPE_VIBRATION_CHAIN, VibrationCable],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, dragerGasDongle],
|
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, dragerGasDongle],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, Airflow]
|
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, Airflow],
|
||||||
|
[quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE, AnalogPressure]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export interface Subtype {
|
export interface Subtype {
|
||||||
|
|
@ -532,7 +534,7 @@ export function simpleLineChartData(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(lineData)
|
// console.log(lineData)
|
||||||
return lineData;
|
return lineData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
88
src/pbHelpers/ComponentTypes/AnalogPressure.ts
Normal file
88
src/pbHelpers/ComponentTypes/AnalogPressure.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
ComponentTypeExtension,
|
||||||
|
Summary,
|
||||||
|
Subtype,
|
||||||
|
simpleMeasurements,
|
||||||
|
simpleSummaries,
|
||||||
|
unitMeasurementSummaries,
|
||||||
|
AreaChartData,
|
||||||
|
GraphFilters,
|
||||||
|
simpleAreaChartData,
|
||||||
|
LineChartData,
|
||||||
|
simpleLineChartData
|
||||||
|
} from "pbHelpers/ComponentType";
|
||||||
|
import PressureDarkIcon from "assets/components/pressureDark.png";
|
||||||
|
import PressureLightIcon from "assets/components/pressureLight.png";
|
||||||
|
import { quack } from "protobuf-ts/quack";
|
||||||
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||||
|
import { convertedUnitMeasurement } from "models/UnitMeasurement";
|
||||||
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
||||||
|
export function AnalogPressure(subtype: number = 0): ComponentTypeExtension {
|
||||||
|
let pressure = describeMeasurement(
|
||||||
|
quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE,
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE,
|
||||||
|
subtype
|
||||||
|
);
|
||||||
|
let voltage = describeMeasurement(
|
||||||
|
quack.MeasurementType.MEASUREMENT_TYPE_VOLTAGE,
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE,
|
||||||
|
subtype
|
||||||
|
)
|
||||||
|
let addressTypes = [quack.AddressType.ADDRESS_TYPE_I2C];
|
||||||
|
return {
|
||||||
|
type: quack.ComponentType.COMPONENT_TYPE_PRESSURE,
|
||||||
|
subtypes: [
|
||||||
|
{
|
||||||
|
key: quack.AnalogPressureSubtype.ANALOG_PRESSURE_SUBTYPE_PM1618,
|
||||||
|
value: "ANALOG_PRESSURE_SUBTYPE_PM1618",
|
||||||
|
friendlyName: "PM 1618"
|
||||||
|
} as Subtype
|
||||||
|
],
|
||||||
|
friendlyName: "Analog Pressure",
|
||||||
|
description: "Measures the atmospheric pressure or absolute pressure",
|
||||||
|
isController: false,
|
||||||
|
isSource: true,
|
||||||
|
isCalibratable: true,
|
||||||
|
hasFan: true,
|
||||||
|
addressTypes: addressTypes,
|
||||||
|
interactionResultTypes: [],
|
||||||
|
states: [],
|
||||||
|
measurements: simpleMeasurements(pressure, voltage),
|
||||||
|
measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
|
||||||
|
return simpleSummaries(measurement, pressure);
|
||||||
|
},
|
||||||
|
unitMeasurementSummary: (
|
||||||
|
measurements: convertedUnitMeasurement,
|
||||||
|
): Summary[] => {
|
||||||
|
return unitMeasurementSummaries(
|
||||||
|
measurements,
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_PRESSURE,
|
||||||
|
subtype,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
areaChartData: (
|
||||||
|
measurement: pond.UnitMeasurementsForComponent,
|
||||||
|
smoothingAverages?: number,
|
||||||
|
filters?: GraphFilters
|
||||||
|
): AreaChartData => {
|
||||||
|
return simpleAreaChartData(measurement, smoothingAverages, filters);
|
||||||
|
},
|
||||||
|
lineChartData: (
|
||||||
|
measurement: pond.UnitMeasurementsForComponent,
|
||||||
|
smoothingAverages?: number,
|
||||||
|
filters?: GraphFilters
|
||||||
|
): LineChartData => {
|
||||||
|
return simpleLineChartData(
|
||||||
|
quack.ComponentType.COMPONENT_TYPE_PRESSURE,
|
||||||
|
measurement,
|
||||||
|
smoothingAverages,
|
||||||
|
filters
|
||||||
|
);
|
||||||
|
},
|
||||||
|
minMeasurementPeriodMs: 1000,
|
||||||
|
icon: (theme?: "light" | "dark"): string | undefined => {
|
||||||
|
return theme === "light" ? PressureDarkIcon : PressureLightIcon;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -29,3 +29,4 @@ export * from "./Voltage";
|
||||||
export * from "./VPD";
|
export * from "./VPD";
|
||||||
export * from "./Weight";
|
export * from "./Weight";
|
||||||
export * from "./CapacitorCable";
|
export * from "./CapacitorCable";
|
||||||
|
export * from "./AnalogPressure";
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,8 @@ const DefaultAvailability: DeviceAvailabilityMap = new Map<quack.AddressType, De
|
||||||
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
|
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
|
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], // the address cables will use when they are plugged into the expander with V2 device only
|
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], // the address cables will use when they are plugged into the expander with V2 device only
|
||||||
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]]
|
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]],
|
||||||
|
[quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE, [0x6e]]
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
[quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]],
|
[quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]],
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export interface FirmwareVersionHelper {
|
||||||
|
|
||||||
const oldFirmwareColour = "var(--status-warning)";
|
const oldFirmwareColour = "var(--status-warning)";
|
||||||
const currentFirmwareColour = "var(--status-ok)";
|
const currentFirmwareColour = "var(--status-ok)";
|
||||||
|
const unknownFirmwareColour = "var(--status-warning)";
|
||||||
|
|
||||||
export function getFirmwareVersionHelper(
|
export function getFirmwareVersionHelper(
|
||||||
version: string,
|
version: string,
|
||||||
|
|
@ -17,24 +18,35 @@ export function getFirmwareVersionHelper(
|
||||||
const firmwareIcon = <FirmwareIcon />;
|
const firmwareIcon = <FirmwareIcon />;
|
||||||
const oldFirmwareIcon = <FirmwareIcon style={{ color: oldFirmwareColour }} />;
|
const oldFirmwareIcon = <FirmwareIcon style={{ color: oldFirmwareColour }} />;
|
||||||
const currentFirmwareIcon = <FirmwareIcon style={{ color: currentFirmwareColour }} />;
|
const currentFirmwareIcon = <FirmwareIcon style={{ color: currentFirmwareColour }} />;
|
||||||
|
const unknownFirmwareIcon = <FirmwareIcon style={{ color: unknownFirmwareColour }} />;
|
||||||
let helper = {} as FirmwareVersionHelper;
|
let helper = {} as FirmwareVersionHelper;
|
||||||
if (!version || version === "") {
|
if (!version || version === "") {
|
||||||
helper.description = "Unknown version";
|
helper.description = "Unknown version";
|
||||||
helper.icon = oldFirmwareIcon;
|
helper.icon = oldFirmwareIcon;
|
||||||
helper.tooltip = "We don't know what version your device is, it may behave unexpectedly";
|
helper.tooltip = "We don't know what version your device is, it may behave unexpectedly";
|
||||||
|
helper.colour = oldFirmwareColour;
|
||||||
|
} else if (version.startsWith("!")) {
|
||||||
|
const cleanVersion = version.substring(1);
|
||||||
|
helper.description = cleanVersion;
|
||||||
|
helper.icon = unknownFirmwareIcon;
|
||||||
|
helper.tooltip = "Firmware " + cleanVersion + " was reported by the device but is missing from the firmware list";
|
||||||
|
helper.colour = unknownFirmwareColour;
|
||||||
} else if (available !== "" && version !== available) {
|
} else if (available !== "" && version !== available) {
|
||||||
helper.description = version;
|
helper.description = version;
|
||||||
helper.icon = oldFirmwareIcon;
|
helper.icon = oldFirmwareIcon;
|
||||||
helper.tooltip =
|
helper.tooltip =
|
||||||
"A firmware upgrade is available, some features may be disabled for out-of-date devices";
|
"A firmware upgrade is available, some features may be disabled for out-of-date devices";
|
||||||
|
helper.colour = oldFirmwareColour;
|
||||||
} else if (available === "") {
|
} else if (available === "") {
|
||||||
helper.description = version;
|
helper.description = version;
|
||||||
helper.icon = firmwareIcon;
|
helper.icon = firmwareIcon;
|
||||||
helper.tooltip = "Running version " + version;
|
helper.tooltip = "Running version " + version;
|
||||||
|
helper.colour = "";
|
||||||
} else {
|
} else {
|
||||||
helper.description = version;
|
helper.description = version;
|
||||||
helper.icon = currentFirmwareIcon;
|
helper.icon = currentFirmwareIcon;
|
||||||
helper.tooltip = "Your device is running the latest firmware";
|
helper.tooltip = "Your device is running the latest firmware";
|
||||||
|
helper.colour = currentFirmwareColour;
|
||||||
}
|
}
|
||||||
return helper;
|
return helper;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,10 +174,9 @@ export class MeasurementDescriber {
|
||||||
this.details.path = "power.inputVoltageTimes10";
|
this.details.path = "power.inputVoltageTimes10";
|
||||||
this.details.decimals = 1;
|
this.details.decimals = 1;
|
||||||
}
|
}
|
||||||
if (componentType === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) {
|
if (componentType === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE || componentType === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE) {
|
||||||
this.details.unit = "mV";
|
this.details.unit = "mV";
|
||||||
this.details.graph = GraphType.MULTILINE;
|
this.details.graph = GraphType.MULTILINE;
|
||||||
this.details.unit = "mV";
|
|
||||||
this.details.decimals = 0
|
this.details.decimals = 0
|
||||||
// this.details.nodeDetails = {
|
// this.details.nodeDetails = {
|
||||||
// colours: ["white", "black", "red", "blue"],
|
// colours: ["white", "black", "red", "blue"],
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import { cyan } from "@mui/material/colors";
|
|
||||||
import AcceptedIcon from "@mui/icons-material/CloudDone";
|
|
||||||
import PendingIcon from "@mui/icons-material/CloudQueueTwoTone";
|
|
||||||
import RejectedIcon from "@mui/icons-material/SmsFailed";
|
|
||||||
|
|
||||||
export interface StatusHelper {
|
|
||||||
description: string;
|
|
||||||
icon: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Unknown: StatusHelper = {
|
|
||||||
description: "Status unknown",
|
|
||||||
icon: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const Pending: StatusHelper = {
|
|
||||||
description: "Pending changes",
|
|
||||||
icon: <PendingIcon style={{ color: cyan["600"] }} />
|
|
||||||
};
|
|
||||||
|
|
||||||
const Stale: StatusHelper = {
|
|
||||||
description: "Stale update",
|
|
||||||
icon: null
|
|
||||||
};
|
|
||||||
|
|
||||||
const Accepted: StatusHelper = {
|
|
||||||
description: "Settings synced",
|
|
||||||
icon: <AcceptedIcon style={{ color: "var(--status-ok)" }} />
|
|
||||||
};
|
|
||||||
|
|
||||||
const Rejected: StatusHelper = {
|
|
||||||
description: "Update failed",
|
|
||||||
icon: <RejectedIcon style={{ color: "var(--status-warning)" }} />
|
|
||||||
};
|
|
||||||
|
|
||||||
const Received: StatusHelper = {
|
|
||||||
description: "Settings synced",
|
|
||||||
icon: <AcceptedIcon style={{ color: "var(--status-ok)" }} />
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_MAP = new Map<string, StatusHelper>([
|
|
||||||
["pending", Pending],
|
|
||||||
["stale", Stale],
|
|
||||||
["accepted", Accepted],
|
|
||||||
["rejected", Rejected],
|
|
||||||
["received", Received]
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function getStatusHelper(status: string): StatusHelper {
|
|
||||||
const statuses = Array.from(STATUS_MAP.keys());
|
|
||||||
for (var i = 0; i < statuses.length; i++) {
|
|
||||||
let key = statuses[i];
|
|
||||||
if (status === key) {
|
|
||||||
return STATUS_MAP.get(key) as StatusHelper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStatusDescription(status: string): string {
|
|
||||||
return getStatusHelper(status).description;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStatusIcon(status: string): any {
|
|
||||||
return getStatusHelper(status).icon;
|
|
||||||
}
|
|
||||||
|
|
@ -10,6 +10,7 @@ const MiVentV1Pins: ConfigurablePin[] = [
|
||||||
{ address: 1024, label: "C2" }
|
{ address: 1024, label: "C2" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//no longer in development so if you are adding new component types DO NOT add the I2C address here because the firmware of the device will not support it
|
||||||
export const MiVentV1Availability: DeviceAvailabilityMap = new Map<
|
export const MiVentV1Availability: DeviceAvailabilityMap = new Map<
|
||||||
quack.AddressType,
|
quack.AddressType,
|
||||||
DevicePositions
|
DevicePositions
|
||||||
|
|
@ -53,7 +54,8 @@ export const MiVentV2Availability: DeviceAvailabilityMap = new Map<
|
||||||
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
|
[quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
|
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
|
[quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
|
||||||
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]]
|
[quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]],
|
||||||
|
[quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE, [0x6e]]
|
||||||
|
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,14 @@ export interface IComponentAPIContext {
|
||||||
keys?: string[],
|
keys?: string[],
|
||||||
types?: string[]
|
types?: string[]
|
||||||
) => Promise<AxiosResponse<pond.ListComponentHistoryResponse>>;
|
) => Promise<AxiosResponse<pond.ListComponentHistoryResponse>>;
|
||||||
|
listHistoryBetween: (
|
||||||
|
deviceId: string | number,
|
||||||
|
componentKey: string,
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
keys?: string[],
|
||||||
|
types?: string[]
|
||||||
|
) => Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>;
|
||||||
// Old measuremnt structure functions, no longer used in codebase
|
// Old measuremnt structure functions, no longer used in codebase
|
||||||
// listMeasurements: (
|
// listMeasurements: (
|
||||||
// device: number,
|
// device: number,
|
||||||
|
|
@ -391,6 +399,37 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listHistoryBetween = (
|
||||||
|
deviceId: string | number,
|
||||||
|
componentKey: string,
|
||||||
|
start: string,
|
||||||
|
end: string,
|
||||||
|
keys?: string[],
|
||||||
|
types?: string[]
|
||||||
|
) => {
|
||||||
|
let url = pondURL(
|
||||||
|
"/devices/" +
|
||||||
|
deviceId +
|
||||||
|
"/components/" +
|
||||||
|
componentKey +
|
||||||
|
"/historyBetween?start=" +
|
||||||
|
start +
|
||||||
|
"&end=" +
|
||||||
|
end +
|
||||||
|
(keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) +
|
||||||
|
(types ? "&types=" + types : "&types=" + ["device"]) +
|
||||||
|
(as ? "&as=" + as : "")
|
||||||
|
)
|
||||||
|
return new Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>((resolve,reject) => {
|
||||||
|
get<pond.ListComponentHistoryBetweenResponse>(url).then(resp => {
|
||||||
|
resp.data = pond.ListComponentHistoryBetweenResponse.fromObject(resp.data)
|
||||||
|
return resolve(resp)
|
||||||
|
}).catch(err => {
|
||||||
|
return reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// const listMeasurements = (
|
// const listMeasurements = (
|
||||||
// device: number,
|
// device: number,
|
||||||
// component: string,
|
// component: string,
|
||||||
|
|
@ -596,6 +635,7 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
|
||||||
listForObject: listComponentsForObject,
|
listForObject: listComponentsForObject,
|
||||||
listComponentCardData: listComponentCardData,
|
listComponentCardData: listComponentCardData,
|
||||||
listHistory,
|
listHistory,
|
||||||
|
listHistoryBetween,
|
||||||
//listMeasurements: listMeasurements,
|
//listMeasurements: listMeasurements,
|
||||||
//sampleMeasurements: sampleMeasurements,
|
//sampleMeasurements: sampleMeasurements,
|
||||||
sampleUnitMeasurements,
|
sampleUnitMeasurements,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ const protips: string[] = [
|
||||||
"Want to receive text messages about your devices? Update your phone number and enable SMS notifications!"
|
"Want to receive text messages about your devices? Update your phone number and enable SMS notifications!"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEFAULT_HEADER_COLOUR = "#1f2a35";
|
||||||
|
|
||||||
export interface WhiteLabelFeatures {
|
export interface WhiteLabelFeatures {
|
||||||
bins: boolean;
|
bins: boolean;
|
||||||
visualFarm: boolean;
|
visualFarm: boolean;
|
||||||
|
|
@ -55,6 +57,7 @@ interface WhiteLabel {
|
||||||
secondaryColour: any;
|
secondaryColour: any;
|
||||||
signatureColour: any;
|
signatureColour: any;
|
||||||
signatureAccentColour: any;
|
signatureAccentColour: any;
|
||||||
|
headerColour?: any;
|
||||||
auth0ClientId: string;
|
auth0ClientId: string;
|
||||||
redirectOnLogout: boolean;
|
redirectOnLogout: boolean;
|
||||||
logoutRedirectTarget: string;
|
logoutRedirectTarget: string;
|
||||||
|
|
@ -444,6 +447,14 @@ export function getSignatureColour(): any {
|
||||||
return getWhitelabel().signatureColour;
|
return getWhitelabel().signatureColour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHeaderColour(): any {
|
||||||
|
return getWhitelabel().headerColour ?? DEFAULT_HEADER_COLOUR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHeaderColor(): any {
|
||||||
|
return getHeaderColour();
|
||||||
|
}
|
||||||
|
|
||||||
export function getSignatureAccentColour(): any {
|
export function getSignatureAccentColour(): any {
|
||||||
return getWhitelabel().signatureAccentColour;
|
return getWhitelabel().signatureAccentColour;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,21 @@
|
||||||
import * as Colours from "@mui/material/colors"
|
import * as Colours from "@mui/material/colors"
|
||||||
import { createTheme, ThemeOptions } from '@mui/material/styles';
|
import { createTheme, ThemeOptions } from '@mui/material/styles';
|
||||||
import {
|
import {
|
||||||
|
getHeaderColor,
|
||||||
getPrimaryColour,
|
getPrimaryColour,
|
||||||
getSecondaryColour,
|
getSecondaryColour,
|
||||||
} from "../services/whiteLabel";
|
} from "../services/whiteLabel";
|
||||||
|
|
||||||
|
declare module "@mui/material/styles" {
|
||||||
|
interface Palette {
|
||||||
|
header: Palette["primary"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaletteOptions {
|
||||||
|
header?: PaletteOptions["primary"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const baseTheme: ThemeOptions = {
|
const baseTheme: ThemeOptions = {
|
||||||
palette: {
|
palette: {
|
||||||
mode: 'light', // default mode; will be overridden by system if needed
|
mode: 'light', // default mode; will be overridden by system if needed
|
||||||
|
|
@ -62,6 +73,9 @@ export const getTheme = (mode: 'light' | 'dark') =>
|
||||||
...baseTheme.palette,
|
...baseTheme.palette,
|
||||||
primary: makePaletteColor(getPrimaryColour()),
|
primary: makePaletteColor(getPrimaryColour()),
|
||||||
secondary: makePaletteColor(getSecondaryColour()),
|
secondary: makePaletteColor(getSecondaryColour()),
|
||||||
|
header: {
|
||||||
|
main: getHeaderColor(),
|
||||||
|
},
|
||||||
mode,
|
mode,
|
||||||
...(mode === 'dark'
|
...(mode === 'dark'
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@ export * from "./download";
|
||||||
export * from "./environment";
|
export * from "./environment";
|
||||||
export * from "./numbers";
|
export * from "./numbers";
|
||||||
export * from "./strings";
|
export * from "./strings";
|
||||||
|
export * from "./syncStatus";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
export * from "./units";
|
export * from "./units";
|
||||||
|
|
|
||||||
41
src/utils/syncStatus.ts
Normal file
41
src/utils/syncStatus.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
export function getTimestampMillis(timestamp?: string): number | undefined {
|
||||||
|
if (!timestamp) return undefined;
|
||||||
|
const parsed = Date.parse(timestamp);
|
||||||
|
return Number.isNaN(parsed) ? undefined : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The sync-tracking fields shared by ComponentStatus and InteractionStatus. */
|
||||||
|
export interface SyncStatusLike {
|
||||||
|
synced: boolean;
|
||||||
|
lastUpdate?: string;
|
||||||
|
lastSynced?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides whether an incoming websocket status message is stale and should
|
||||||
|
* not replace the status we already hold. A message is stale when it was
|
||||||
|
* generated before our latest local update, including a synced echo that
|
||||||
|
* predates a pending change we just submitted.
|
||||||
|
*/
|
||||||
|
export function isStaleStatusUpdate(existing: SyncStatusLike, incoming: SyncStatusLike): boolean {
|
||||||
|
const existingLastUpdate = getTimestampMillis(existing.lastUpdate);
|
||||||
|
const incomingLastUpdate = getTimestampMillis(incoming.lastUpdate);
|
||||||
|
const incomingLastSynced = getTimestampMillis(incoming.lastSynced);
|
||||||
|
if (
|
||||||
|
existingLastUpdate !== undefined &&
|
||||||
|
incomingLastUpdate !== undefined &&
|
||||||
|
existingLastUpdate > incomingLastUpdate
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!existing.synced &&
|
||||||
|
incoming.synced &&
|
||||||
|
incomingLastUpdate !== undefined &&
|
||||||
|
existingLastUpdate === incomingLastUpdate &&
|
||||||
|
(incomingLastSynced === undefined || incomingLastSynced < incomingLastUpdate)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue