Compare commits

..

43 commits

Author SHA1 Message Date
Carter
696fbb3593 wrapping scrollIntoView in {} to prevent it from returning void 2026-07-21 11:59:48 -06:00
Carter
ddd0b5062b settings forms to prevent it from revert user changed 2026-07-17 14:58:31 -06:00
Carter
80750bd3e9 hotfix: mode node id can be anything > 0 2026-07-16 14:22:33 -06:00
Carter
65f8fd06d3 hotfix: mode node id inputs 2026-07-16 12:47:56 -06:00
Carter
199511e829 Merge branch 'master' of gitlab.com:brandx/bxt-app 2026-07-13 13:02:28 -06:00
Carter
b272d264b6 updated master proto 2026-07-13 13:01:59 -06:00
Carter
cb168aef9b Merge branch 'tag_fix3' into staging_environment 2026-07-10 10:12:01 -06:00
Carter
d124db62e0 got rid of scanned logs 2026-07-10 10:11:46 -06:00
Carter
de662907c6 Merge branch 'tag_fix3' into staging_environment 2026-07-09 11:58:39 -06:00
Carter
487afc6464 removed component type logs 2026-07-09 11:58:32 -06:00
csawatzky
fc347938d2 hotfix for analog pressure, is shoudl support expansion and not cable ID 2026-07-09 11:32:32 -06:00
Carter
72c920404c Merge branch 'tag_fix3' into staging_environment 2026-07-08 16:10:24 -06:00
Carter
3fc663a4b6 removed interaction logs 2026-07-08 15:56:03 -06:00
csawatzky
2c17fb655c hotfix to add as to the historyBetween function in the component API so that it works when viewing as a team without direct user permission to it 2026-07-08 11:01:45 -06:00
Carter
71fb67bba4 pending changes indication for device and component changes 2026-07-07 11:28:34 -06:00
Carter
10f0aa46cc merged staging 2026-06-30 10:51:17 -06:00
Carter
8706446108 proto update and audit 2026-06-30 10:50:29 -06:00
Carter
9534eed94c Merge branch 'new_header' into staging_environment 2026-06-30 09:47:51 -06:00
Carter
3eb66e3622 changed header to match new theme 2026-06-30 09:43:38 -06:00
csawatzky
5dc3a3e975 Merge branch 'bin_player_inventory_changes' into staging_environment 2026-06-29 14:17:48 -06:00
csawatzky
7666fbf9b6 update to the bin player to set the bin bushels in the status using the measurements from the bin measurements 2026-06-26 15:25:18 -06:00
Carter
64fcc934ce Merge branch 'email_control' into staging_environment 2026-06-25 14:04:04 -06:00
Carter
cca981308d Merge branch 'pending_changes' into staging_environment 2026-06-25 12:48:28 -06:00
Carter
ffd5e0a769 preventing form updates when settings dialog is open 2026-06-25 12:33:14 -06:00
Carter
e8cc509915 less restrictive on receiving websockets 2026-06-25 12:15:49 -06:00
Carter
b9a13c5781 fixed pending change race condition 2026-06-24 14:40:04 -06:00
Carter
879a5ae1f0 fixed pending state logic 2026-06-24 14:16:54 -06:00
Carter
895ed8e50b opening interaction websocket to detect accepted pending changes 2026-06-24 13:13:14 -06:00
csawatzky
4ea0d53c94 adding analog pressure to have the cable id show up for analog cables 2026-06-24 12:38:29 -06:00
Carter
6b07859d00 Merge branch 'firmware_mismatch' into pending_changes 2026-06-24 12:13:50 -06:00
csawatzky
f73eb0fcbd proto update 2026-06-23 16:44:42 -06:00
csawatzky
728d07a07e proto update 2026-06-23 16:42:30 -06:00
csawatzky
e19e1f03eb Merge branch 'analog_pressure_component' into staging_environment 2026-06-23 16:41:36 -06:00
csawatzky
270df1a118 fixed unit from V to mV for analog pressure 2026-06-23 16:40:09 -06:00
csawatzky
939a53d072 had claude look at the bin player to determine how to take into account the component history for the top nodes 2026-06-23 16:25:31 -06:00
Carter
0984ae0f37 got rid of warning signifier on firmware print 2026-06-17 16:19:35 -06:00
Carter
d9846ea02b firmware renders with warning styling if it wasn't found in the server 2026-06-17 15:41:26 -06:00
Carter
b378764d62 added some info about email control to the control tab for the component settings 2026-06-17 14:11:43 -06:00
Carter
c4999a0ba5 adding control emails to control component settings form 2026-06-16 12:32:43 -06:00
csawatzky
1051534bb5 proto update to stagings proto 2026-06-12 14:58:30 -06:00
csawatzky
bbc9c4ee7f changing some naming for the component 2026-06-12 14:43:37 -06:00
csawatzky
c0a3e23d67 missed the device availability stuff 2026-04-21 16:13:59 -06:00
csawatzky
90ad5f8b12 new component type 2026-04-21 15:38:14 -06:00
41 changed files with 1311 additions and 677 deletions

412
package-lock.json generated
View file

@ -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#plenum_ambient_component", "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#1a3b63d5a2f4c9165642d994c842abecc3f3bffc", "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",

View file

@ -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#plenum_ambient_component", "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",

View file

@ -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),
@ -128,4 +128,4 @@ export default function Header() {
{isMobile && <BottomNavigator setNavOpen={isOpen => {setNavOpen(isOpen)}} sideIsOpen={navOpen} />} {isMobile && <BottomNavigator setNavOpen={isOpen => {setNavOpen(isOpen)}} sideIsOpen={navOpen} />}
</> </>
) )
} }

View file

@ -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);
@ -715,4 +867,4 @@ export default function BinPlayer(props: Props) {
</Box> </Box>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -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)
}}> }}>

View file

@ -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;
}; };

View 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;
}

View 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>
);
}

View file

@ -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;
}

View file

@ -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>

View file

@ -19,9 +19,10 @@ import {
Tooltip, Tooltip,
Typography Typography
} 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 && (

View file

@ -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
}, },
{ {

View file

@ -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,36 +1210,55 @@ export default function ComponentSettings(props: Props) {
scroll="paper"> scroll="paper">
{title()} {title()}
<Divider /> <Divider />
{!isController(formComponent.settings.type) && ( {isController(formComponent.settings.type) ? (
<Tabs <React.Fragment>
value={tabVal} <Tabs
indicatorColor="primary" value={tabVal}
textColor="primary" indicatorColor="primary"
onChange={handleChange}> textColor="primary"
<Tab label="General" /> onChange={handleChange}>
<Tab label="Overlays" /> <Tab label="General" />
<Tab label="Nodes" disabled={!prevComponent} /> <Tab label="Control" />
</Tabs> </Tabs>
<TabPanelMine value={tabVal} index={0}>
{content()}
</TabPanelMine>
<TabPanelMine value={tabVal} index={1}>
{controlContent()}
</TabPanelMine>
</React.Fragment>
) : (
<React.Fragment>
<Tabs
value={tabVal}
indicatorColor="primary"
textColor="primary"
onChange={handleChange}>
<Tab label="General" />
<Tab label="Overlays" />
<Tab label="Nodes" disabled={!prevComponent} />
</Tabs>
<TabPanelMine value={tabVal} index={0}>
{content()}
</TabPanelMine>
<TabPanelMine value={tabVal} index={1}>
<DialogContent>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
alignContent="center"
spacing={1}>
{overlayGroup()}
</Grid>
</DialogContent>
</TabPanelMine>
<TabPanelMine value={tabVal} index={2}>
{componentPrefs()}
</TabPanelMine>
</React.Fragment>
)} )}
<TabPanelMine value={tabVal} index={0}>
{content()}
</TabPanelMine>
<TabPanelMine value={tabVal} index={1}>
<DialogContent>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
alignContent="center"
spacing={1}>
{overlayGroup()}
</Grid>
</DialogContent>
</TabPanelMine>
<TabPanelMine value={tabVal} index={2}>
{componentPrefs()}
</TabPanelMine>
{actions()} {actions()}
</ResponsiveDialog> </ResponsiveDialog>
)} )}

View file

@ -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 && (

View file

@ -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 (components) {
if (device.settings.mutations) { let cbd = new Map<string, Component[]>();
setLinearMutations(device.settings.mutations); cbd.set(settingsForm.id().toString(), components);
} setComponentsByDevice(cbd);
if (components) {
let cbd = new Map<string, Component[]>();
cbd.set(device.id().toString(), components);
setComponentsByDevice(cbd);
}
} }
}, [device, prevDevice, props.device, components]); }, [device, components, isDialogOpen, prevIsDialogOpen]);
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>

View file

@ -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>
); );

View file

@ -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")

View file

@ -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";

View 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
};
}

View file

@ -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;

View file

@ -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}
/> />

View file

@ -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()
} }

View file

@ -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 >

View file

@ -40,9 +40,7 @@ import {
VPD, VPD,
Weight, Weight,
CapacitorCable, CapacitorCable,
Plenum, AnalogPressure
AmbientOnewire,
AmbientI2C
} 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";
@ -100,10 +98,7 @@ const COMPONENT_TYPE_MAP = new Map<quack.ComponentType, Function>([
[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_PLENUM, Plenum], [quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE, AnalogPressure]
[quack.ComponentType.COMPONENT_TYPE_AMBIENT_ONEWIRE, AmbientOnewire],
[quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C, AmbientI2C],
]); ]);
export interface Subtype { export interface Subtype {
@ -539,7 +534,7 @@ export function simpleLineChartData(
} }
}); });
} }
console.log(lineData) // console.log(lineData)
return lineData; return lineData;
} }

View file

@ -1,79 +0,0 @@
import TemperatureHumidityDarkIcon from "assets/components/temperatureHumidityDark.png";
import TemperatureHumidityLightIcon from "assets/components/temperatureHumidityLight.png";
import { convertedUnitMeasurement } from "models/UnitMeasurement";
import {
AreaChartData,
ComponentTypeExtension,
GraphFilters,
LineChartData,
simpleAreaChartData,
simpleLineChartData,
simpleMeasurements,
simpleSummaries,
Summary,
unitMeasurementSummaries
} from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
export function AmbientI2C(subtype: number = 0): ComponentTypeExtension {
let temperature = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C,
subtype
);
let humidity = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C,
subtype
);
return {
type: quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C,
subtypes: [],
friendlyName: "Ambient I2C",
description: "Measures temperature and humidity",
isController: false,
isSource: true,
isCalibratable: true,
addressTypes: [quack.AddressType.ADDRESS_TYPE_I2C],
interactionResultTypes: [],
states: [],
measurements: simpleMeasurements(temperature, humidity),
measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
return simpleSummaries(measurement, temperature, humidity);
},
unitMeasurementSummary: (
measurements: convertedUnitMeasurement
): Summary[] => {
return unitMeasurementSummaries(
measurements,
quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C,
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_AMBIENT_I2C,
measurement,
smoothingAverages,
filters
);
},
minMeasurementPeriodMs: 1000,
icon: (theme?: "light" | "dark"): string | undefined => {
return theme === "light" ? TemperatureHumidityDarkIcon : TemperatureHumidityLightIcon;
}
};
}

View file

@ -1,79 +0,0 @@
import TemperatureHumidityDarkIcon from "assets/components/temperatureHumidityDark.png";
import TemperatureHumidityLightIcon from "assets/components/temperatureHumidityLight.png";
import { convertedUnitMeasurement } from "models/UnitMeasurement";
import {
AreaChartData,
ComponentTypeExtension,
GraphFilters,
LineChartData,
simpleAreaChartData,
simpleLineChartData,
simpleMeasurements,
simpleSummaries,
Summary,
unitMeasurementSummaries
} from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
export function AmbientOnewire(subtype: number = 0): ComponentTypeExtension {
let temperature = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
quack.ComponentType.COMPONENT_TYPE_AMBIENT_ONEWIRE,
subtype
);
let humidity = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
quack.ComponentType.COMPONENT_TYPE_AMBIENT_ONEWIRE,
subtype
);
return {
type: quack.ComponentType.COMPONENT_TYPE_AMBIENT_ONEWIRE,
subtypes: [],
friendlyName: "Onewire Ambient",
description: "Measures temperature and humidity",
isController: false,
isSource: true,
isCalibratable: true,
addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY],
interactionResultTypes: [],
states: [],
measurements: simpleMeasurements(temperature, humidity),
measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
return simpleSummaries(measurement, temperature, humidity);
},
unitMeasurementSummary: (
measurements: convertedUnitMeasurement
): Summary[] => {
return unitMeasurementSummaries(
measurements,
quack.ComponentType.COMPONENT_TYPE_AMBIENT_ONEWIRE,
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_AMBIENT_ONEWIRE,
measurement,
smoothingAverages,
filters
);
},
minMeasurementPeriodMs: 1000,
icon: (theme?: "light" | "dark"): string | undefined => {
return theme === "light" ? TemperatureHumidityDarkIcon : TemperatureHumidityLightIcon;
}
};
}

View 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;
}
};
}

View file

@ -1,84 +0,0 @@
import TemperatureHumidityDarkIcon from "assets/components/temperatureHumidityDark.png";
import TemperatureHumidityLightIcon from "assets/components/temperatureHumidityLight.png";
import { convertedUnitMeasurement } from "models/UnitMeasurement";
import {
AreaChartData,
ComponentTypeExtension,
GraphFilters,
LineChartData,
simpleAreaChartData,
simpleLineChartData,
simpleMeasurements,
simpleSummaries,
Summary,
unitMeasurementSummaries
} from "pbHelpers/ComponentType";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
export function Plenum(subtype: number = 0): ComponentTypeExtension {
let temperature = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
quack.ComponentType.COMPONENT_TYPE_PLENUM,
subtype
);
let humidity = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
quack.ComponentType.COMPONENT_TYPE_PLENUM,
subtype
);
let pressure = describeMeasurement(
quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE,
quack.ComponentType.COMPONENT_TYPE_PLENUM,
subtype
);
return {
type: quack.ComponentType.COMPONENT_TYPE_PLENUM,
subtypes: [],
friendlyName: "Plenum",
description: "Measures temperature, humidity and pressure",
isController: false,
isSource: true,
isCalibratable: true,
addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY],
interactionResultTypes: [],
states: [],
measurements: simpleMeasurements(temperature, humidity, pressure),
measurementSummary: async function(measurement: quack.Measurement): Promise<Array<Summary>> {
return simpleSummaries(measurement, temperature, humidity, pressure);
},
unitMeasurementSummary: (
measurements: convertedUnitMeasurement
): Summary[] => {
return unitMeasurementSummaries(
measurements,
quack.ComponentType.COMPONENT_TYPE_PLENUM,
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_PLENUM,
measurement,
smoothingAverages,
filters
);
},
minMeasurementPeriodMs: 1000,
icon: (theme?: "light" | "dark"): string | undefined => {
return theme === "light" ? TemperatureHumidityDarkIcon : TemperatureHumidityLightIcon;
}
};
}

View file

@ -29,6 +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 "./Plenum"; export * from "./AnalogPressure";
export * from "./AmbientOnewire";
export * from "./AmbientI2C";

View file

@ -65,7 +65,7 @@ const DefaultAvailability: DeviceAvailabilityMap = new Map<quack.AddressType, De
[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_AMBIENT_I2C, [0x44]] [quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE, [0x6e]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]], [quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]],

View file

@ -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;
} }

View file

@ -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"],

View file

@ -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;
}

View file

@ -252,8 +252,7 @@ export const BindaptV2MonitorAvailability: DeviceAvailabilityMap = new Map<
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]], [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]]
[quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C, [0x44]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]], [quack.AddressType.ADDRESS_TYPE_POWER, [0]],
@ -286,8 +285,7 @@ export const BindaptV2AutomateAvailability: DeviceAvailabilityMap = new Map<
[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]], [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62, 0x66, 0x10]],
[quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
[quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
[quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]]
[quack.ComponentType.COMPONENT_TYPE_AMBIENT_I2C, [0x44]]
]) ])
], ],
[quack.AddressType.ADDRESS_TYPE_POWER, [0]], [quack.AddressType.ADDRESS_TYPE_POWER, [0]],

View file

@ -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]]
]) ])
], ],

View file

@ -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,

View file

@ -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;
} }

View file

@ -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'
? { ? {

View file

@ -169,7 +169,6 @@ export default function ObjectUsers(props: Props) {
userAPI userAPI
.listObjectUsers(scope) .listObjectUsers(scope)
.then((response: any) => { .then((response: any) => {
console.log(response)
let rUsers: User[] = []; let rUsers: User[] = [];
or(response.data, { users: [] }).users.forEach((user: any) => { or(response.data, { users: [] }).users.forEach((user: any) => {
rUsers.push(User.any(user)); rUsers.push(User.any(user));

View file

@ -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
View 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;
}