Compare commits

...
Sign in to create a new pull request.

19 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
27 changed files with 611 additions and 420 deletions

412
package-lock.json generated
View file

@ -46,7 +46,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@ -146,9 +146,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"dev": true,
"license": "MIT",
"engines": {
@ -156,21 +156,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
"@babel/helper-compilation-targets": "^7.29.7",
"@babel/helper-module-transforms": "^7.29.7",
"@babel/helpers": "^7.29.7",
"@babel/parser": "^7.29.7",
"@babel/template": "^7.29.7",
"@babel/traverse": "^7.29.7",
"@babel/types": "^7.29.7",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@ -233,14 +233,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
"@babel/compat-data": "^7.29.7",
"@babel/helper-validator-option": "^7.29.7",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@ -482,9 +482,9 @@
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"dev": true,
"license": "MIT",
"engines": {
@ -507,14 +507,14 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/template": "^7.29.7",
"@babel/types": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
@ -2481,9 +2481,9 @@
}
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz",
"integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==",
"version": "0.10.17",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
"license": "Apache-2.0"
},
"node_modules/@mui/core-downloads-tracker": {
@ -3239,28 +3239,27 @@
}
},
"node_modules/@react-spring/animated": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz",
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
"integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==",
"license": "MIT",
"dependencies": {
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
"@react-spring/shared": "~9.7.5",
"@react-spring/types": "~9.7.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/core": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz",
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz",
"integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==",
"license": "MIT",
"dependencies": {
"@react-spring/animated": "~9.6.1",
"@react-spring/rafz": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
"@react-spring/animated": "~9.7.5",
"@react-spring/shared": "~9.7.5",
"@react-spring/types": "~9.7.5"
},
"funding": {
"type": "opencollective",
@ -3271,34 +3270,34 @@
}
},
"node_modules/@react-spring/rafz": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz",
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz",
"integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==",
"license": "MIT"
},
"node_modules/@react-spring/shared": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz",
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz",
"integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==",
"license": "MIT",
"dependencies": {
"@react-spring/rafz": "~9.6.1",
"@react-spring/types": "~9.6.1"
"@react-spring/rafz": "~9.7.5",
"@react-spring/types": "~9.7.5"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/three": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz",
"integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.5.tgz",
"integrity": "sha512-RxIsCoQfUqOS3POmhVHa1wdWS0wyHAUway73uRLp3GAL5U2iYVNdnzQsep6M2NZ994BlW8TcKuMtQHUqOsy6WA==",
"license": "MIT",
"dependencies": {
"@react-spring/animated": "~9.6.1",
"@react-spring/core": "~9.6.1",
"@react-spring/shared": "~9.6.1",
"@react-spring/types": "~9.6.1"
"@react-spring/animated": "~9.7.5",
"@react-spring/core": "~9.7.5",
"@react-spring/shared": "~9.7.5",
"@react-spring/types": "~9.7.5"
},
"peerDependencies": {
"@react-three/fiber": ">=6.0",
@ -3307,45 +3306,44 @@
}
},
"node_modules/@react-spring/types": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz",
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz",
"integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==",
"license": "MIT"
},
"node_modules/@react-three/drei": {
"version": "9.105.6",
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.105.6.tgz",
"integrity": "sha512-JBgYeV36N9N9f1c3o1ZfLYW4rXZA7UQTq32Y8s3DEF6lwj1/y+RP/yq2VG5I8OzUPl7gsmWdy8fpWZgrlAqUpQ==",
"version": "9.122.0",
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.122.0.tgz",
"integrity": "sha512-SEO/F/rBCTjlLez7WAlpys+iGe9hty4rNgjZvgkQeXFSiwqD4Hbk/wNHMAbdd8vprO2Aj81mihv4dF5bC7D0CA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.11.2",
"@mediapipe/tasks-vision": "0.10.8",
"@monogrid/gainmap-js": "^3.0.5",
"@react-spring/three": "~9.6.1",
"@use-gesture/react": "^10.2.24",
"camera-controls": "^2.4.2",
"@babel/runtime": "^7.26.0",
"@mediapipe/tasks-vision": "0.10.17",
"@monogrid/gainmap-js": "^3.0.6",
"@react-spring/three": "~9.7.5",
"@use-gesture/react": "^10.3.1",
"camera-controls": "^2.9.0",
"cross-env": "^7.0.3",
"detect-gpu": "^5.0.28",
"detect-gpu": "^5.0.56",
"glsl-noise": "^0.0.0",
"hls.js": "1.3.5",
"maath": "^0.10.7",
"meshline": "^3.1.6",
"hls.js": "^1.5.17",
"maath": "^0.10.8",
"meshline": "^3.3.1",
"react-composer": "^5.0.3",
"stats-gl": "^2.0.0",
"stats-gl": "^2.2.8",
"stats.js": "^0.17.0",
"suspend-react": "^0.1.3",
"three-mesh-bvh": "^0.7.0",
"three-stdlib": "^2.29.9",
"troika-three-text": "^0.49.0",
"three-mesh-bvh": "^0.7.8",
"three-stdlib": "^2.35.6",
"troika-three-text": "^0.52.0",
"tunnel-rat": "^0.1.2",
"utility-types": "^3.10.0",
"uuid": "^9.0.1",
"zustand": "^3.7.1"
"utility-types": "^3.11.0",
"zustand": "^5.0.1"
},
"peerDependencies": {
"@react-three/fiber": ">=8.0",
"react": ">=18.0",
"react-dom": ">=18.0",
"@react-three/fiber": "^8",
"react": "^18",
"react-dom": "^18",
"three": ">=0.137"
},
"peerDependenciesMeta": {
@ -3366,6 +3364,35 @@
"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": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.18.0.tgz",
@ -6986,9 +7013,9 @@
}
},
"node_modules/axios": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
"integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
@ -6998,16 +7025,16 @@
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"
@ -8050,9 +8077,9 @@
}
},
"node_modules/deeks": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz",
"integrity": "sha512-e7oWH1LzIdv/prMQ7pmlDlaVoL64glqzvNgkgQNgyec9ORPHrT2jaOqMtRyqJuwWjtfb6v+2rk9pmaHj+F137A==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/deeks/-/deeks-3.2.1.tgz",
"integrity": "sha512-D/o0k3pCG1aI1cxb/dDiWmtMc4Rh7ZQBybXpfMsw9Rbtqwg8kUA9SpYkWcw0pAUjZSnPm8MluctiS0o68r69jQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
@ -8182,9 +8209,9 @@
"license": "MIT"
},
"node_modules/doc-path": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.1.tgz",
"integrity": "sha512-h1ErTglQAVv2gCnOpD3sFS6uolDbOKHDU1BZq+Kl3npPqroU3dYL42lUgMfd5UimlwtRgp7C9dLGwqQ5D2HYgQ==",
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/doc-path/-/doc-path-4.1.4.tgz",
"integrity": "sha512-yw5D++UCIB6a033PvQaUvSpW2QuKW0+DOId763n0Q4z3brxS7G8oQr8yBQ1nQFkognKrAVrV6I55TLeU9cfXTg==",
"license": "MIT",
"engines": {
"node": ">=16"
@ -8357,6 +8384,25 @@
"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": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz",
@ -8416,15 +8462,18 @@
}
},
"node_modules/es-to-primitive": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
"integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz",
"integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-abstract-get": "^1.0.0",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"is-callable": "^1.2.7",
"is-date-object": "^1.0.5",
"is-symbol": "^1.0.4"
"is-date-object": "^1.1.0",
"is-symbol": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
@ -8764,9 +8813,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
"dev": true,
"funding": [
{
@ -8977,15 +9026,15 @@
}
},
"node_modules/form-data": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz",
"integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"hasown": "^2.0.4",
"mime-types": "^2.1.35"
},
"engines": {
@ -9060,18 +9109,21 @@
}
},
"node_modules/function.prototype.name": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
"integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
"integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.3",
"define-properties": "^1.2.1",
"call-bind": "^1.0.9",
"call-bound": "^1.0.4",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"functions-have-names": "^1.2.3",
"hasown": "^2.0.2",
"is-callable": "^1.2.7"
"has-property-descriptors": "^1.0.2",
"hasown": "^2.0.4",
"is-callable": "^1.2.7",
"is-document.all": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
@ -9316,9 +9368,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -9535,9 +9587,9 @@
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -9547,9 +9599,9 @@
}
},
"node_modules/hls.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.5.tgz",
"integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==",
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
"integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
"license": "Apache-2.0"
},
"node_modules/hoist-non-react-statics": {
@ -9913,6 +9965,22 @@
"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": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@ -10356,10 +10424,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@ -10381,13 +10459,13 @@
}
},
"node_modules/json-2-csv": {
"version": "5.5.10",
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.10.tgz",
"integrity": "sha512-Dep8wO3Fr5wNjQevO2Z8Y7yeee/nYSGRsi7q6zJDKEVHxXkXT+v21vxHmDX923UzmCXXkSo62HaTz6eTWzFLaw==",
"version": "5.5.11",
"resolved": "https://registry.npmjs.org/json-2-csv/-/json-2-csv-5.5.11.tgz",
"integrity": "sha512-kVuwgVL7rfad9ETf02ZZxJPuMR5ZSUn139+T34BfmVxYhb/IsAIm/LzEeQ8YLJmXfuQ5z7LUAFrgPZZM6VLJPw==",
"license": "MIT",
"dependencies": {
"deeks": "3.1.0",
"doc-path": "4.1.1"
"deeks": "3.2.1",
"doc-path": "4.1.4"
},
"engines": {
"node": ">= 16"
@ -11631,9 +11709,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"dev": true,
"funding": [
{
@ -11666,9 +11744,9 @@
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"dev": true,
"funding": [
{
@ -11752,7 +11830,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#100126cc7f3cdf18f68af6372e5db4113db012b7",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#d6a992e46192a8ab8195235357318dc297738064",
"dependencies": {
"protobufjs": "^6.8.8"
}
@ -12903,9 +12981,9 @@
}
},
"node_modules/serialize-javascript": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
"integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
"version": "7.0.7",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz",
"integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@ -13681,9 +13759,10 @@
"license": "MIT"
},
"node_modules/three-mesh-bvh": {
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.6.tgz",
"integrity": "sha512-rCjsnxEqR9r1/C/lCqzGLS67NDty/S/eT6rAJfDvsanrIctTWdNoR4ZOGWewCB13h1QkVo2BpmC0wakj1+0m8A==",
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.8.tgz",
"integrity": "sha512-BGEZTOIC14U0XIRw3tO4jY7IjP7n7v24nv9JXS1CyeVRWOCkcOMhRnmENUjuV39gktAw4Ofhr0OvIAiTspQrrw==",
"deprecated": "Deprecated due to three.js version incompatibility. Please use v0.8.0, instead.",
"license": "MIT",
"peerDependencies": {
"three": ">= 0.151.0"
@ -13861,14 +13940,14 @@
}
},
"node_modules/troika-three-text": {
"version": "0.49.1",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz",
"integrity": "sha512-lXGWxgjJP9kw4i4Wh+0k0Q/7cRfS6iOME4knKht/KozPu9GcFA9NnNpRvehIhrUawq9B0ZRw+0oiFHgRO+4Wig==",
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
"license": "MIT",
"dependencies": {
"bidi-js": "^1.0.2",
"troika-three-utils": "^0.49.0",
"troika-worker-utils": "^0.49.0",
"troika-three-utils": "^0.52.4",
"troika-worker-utils": "^0.52.0",
"webgl-sdf-generator": "1.1.1"
},
"peerDependencies": {
@ -13876,18 +13955,18 @@
}
},
"node_modules/troika-three-utils": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.49.0.tgz",
"integrity": "sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA==",
"version": "0.52.4",
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
"license": "MIT",
"peerDependencies": {
"three": ">=0.125.0"
}
},
"node_modules/troika-worker-utils": {
"version": "0.49.0",
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz",
"integrity": "sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg==",
"version": "0.52.0",
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
@ -14341,19 +14420,6 @@
"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": {
"version": "3.0.4",
"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",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",

View file

@ -16,7 +16,7 @@ const useStyles = makeStyles((theme: Theme) => {
appBar: {
zIndex: theme.zIndex.drawer + 1,
backgroundImage: "none", // This prevents de-saturation of header in dark mode
backgroundColor: theme.palette.mode === "light" ? "#3b3b3b" : "#272727"
backgroundColor: theme.palette.header.main
},
toolbar: {
marginLeft: theme.spacing(0.5),
@ -128,4 +128,4 @@ export default function Header() {
{isMobile && <BottomNavigator setNavOpen={isOpen => {setNavOpen(isOpen)}} sideIsOpen={navOpen} />}
</>
)
}
}

View file

@ -24,11 +24,11 @@ export default function ChatOutput(props: Props) {
const ScrollToBottom = () => {
const elementRef = useRef<null | HTMLDivElement>(null);
useEffect(() =>
useEffect(() => {
elementRef?.current?.scrollIntoView({
block: "end"
})
);
});
});
let elem = <div ref={elementRef} />;
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";
import Grid from '@mui/material/Grid2';
import EventBlocker from "common/EventBlocker";
import PendingChangesIndicator from "common/PendingChangesIndicator";
import MeasurementSummary from "component/MeasurementSummary";
import { useComponentAPI, useSnackbar, useThemeType } from "hooks";
import { useComponentAPI, usePendingChanges, useSnackbar, useThemeType } from "hooks";
import InteractionsOverview from "interactions/InteractionsOverview";
import { cloneDeep } from "lodash";
import { Component, Device, Interaction } from "models";
import moment from "moment";
import { getFriendlyAddressTypeName, getHumanReadableAddress } from "pbHelpers/AddressType";
import { controllerModeLabel } from "pbHelpers/Component";
import {
@ -121,6 +123,9 @@ export default function ComponentCard(props: Props) {
const location = useLocation()
const [sensors, setSensors] = useState<Sensor[]>([]);
const [{ user, showErrors, as }] = useGlobalState();
const { pending: pendingChanges, accepted: changesAccepted } = usePendingChanges(
component.status.synced
);
const updateControllerState = (checked: boolean) => {
let updatedComponent = cloneDeep(component);
@ -133,6 +138,8 @@ export default function ComponentCard(props: Props) {
.update(device.id(), updatedComponent.settings, undefined, undefined, as)
.then(() => {
success(component.name() + "'s mode was set to " + describe);
updatedComponent.status.synced = false;
updatedComponent.status.lastUpdate = moment().toISOString();
refreshCallback(updatedComponent);
})
.catch(() => {
@ -287,19 +294,34 @@ export default function ComponentCard(props: Props) {
className={classes.cardHeader}
titleTypographyProps={{ variant: "subtitle1" }}
subheader={
// !newStructure ? (
// <MeasurementSummary
// component={component}
// reading={component.status.lastMeasurement}
// dense
// />
// ) : (
<React.Fragment>
{(pendingChanges || changesAccepted) && (
<Typography variant="caption" color="textSecondary" component="div">
<PendingChangesIndicator
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
component={component}
reading={UnitMeasurement.convertLastMeasurement(measurements)}
//dense
/>
// )
{/* ) */}
</React.Fragment>
}
action={
<EventBlocker>

View file

@ -457,11 +457,29 @@ export default function ComponentForm(props: Props) {
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 = () => {
let calibrate = form.component.settings.calibrate;
let coefficient = Number(form.coefficient);
let valid = !isNaN(coefficient) && (!calibrate || (calibrate && coefficient > 0));
return valid;
if (isNaN(coefficient)) {
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) => {
@ -485,10 +503,14 @@ export default function ComponentForm(props: Props) {
};
const isOffsetValid = (min?: number, max?: number) => {
let minimum = min ?? 0;
let maximum = max ?? 100;
const minimum = min ?? modeDictionaryValue(compMode?.mode.dictionaryB, "min") ?? 0;
const maximum = max ?? modeDictionaryValue(compMode?.mode.dictionaryB, "max") ?? 100;
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));
};
@ -832,6 +854,11 @@ export default function ComponentForm(props: Props) {
onFocus={selectText}
margin="normal"
fullWidth
inputProps={
compMode.mode.stepB !== undefined
? { step: compMode.mode.stepB }
: undefined
}
type={compMode.mode.entryB}
variant="outlined">
{compMode.mode.entryB === "select" &&

View file

@ -110,7 +110,7 @@
"dictionaryA": [
{
"key": "min",
"value": 1,
"value": 0,
"restricted": false
},
{
@ -121,10 +121,12 @@
],
"labelB": "To Node Id",
"entryB": "number",
"exclusiveMinimumB": true,
"stepB": "any",
"dictionaryB": [
{
"key": "min",
"value": 1,
"value": 0,
"restricted": false
},
{

View file

@ -213,15 +213,13 @@ export default function ComponentSettings(props: Props) {
)
);
setFormComponent(initComponent);
setExcludedNodes(initComponent.settings.excludedNodes)
setExcludedNodes(cloneDeep(initComponent.settings.excludedNodes));
}, [props.component, componentTypeOptions]);
useEffect(() => {
if (isDialogOpen && prevIsDialogOpen) return;
if (props.component !== prevComponent || isDialogOpen !== prevIsDialogOpen) {
init();
}
}, [props.component, prevComponent, init, isDialogOpen, prevIsDialogOpen]);
if (!isDialogOpen || prevIsDialogOpen) return;
init();
}, [init, isDialogOpen, prevIsDialogOpen]);
const close = () => {
closeDialogCallback();
@ -518,7 +516,6 @@ export default function ComponentSettings(props: Props) {
formComponent.settings.type ===
quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE ||
(formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE &&
formComponent.subType() ===
quack.CapacitorCableSubtype.CAPACITOR_CABLE_SUBTYPE_FROG))
@ -526,7 +523,9 @@ export default function ComponentSettings(props: Props) {
const supportsExpansion = () => {
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)
}

View file

@ -1,9 +1,9 @@
import { Chip, Grid2 as Grid, Theme, Tooltip, Skeleton } from "@mui/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 VersionChip from "device/VersionChip";
import { useSnackbar, useWidth, usePrevious } from "hooks";
import { useSnackbar, useWidth, usePendingChanges, usePrevious } from "hooks";
import { Device, latestFirmwareVersion, Component } from "models";
import moment from "moment";
import { describeConnectivity } from "pbHelpers/Connectivity";
@ -54,6 +54,10 @@ export default function DeviceOverview(props: Props) {
// const match = useRouteMatch<MatchParams>();
const [powerComponent, setPowerComponent] = 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);
useEffect(() => {
@ -228,9 +232,9 @@ export default function DeviceOverview(props: Props) {
</Tooltip>
</Grid>
)}
{!device.status.synced && (
{(pendingChanges || changesAccepted) && (
<Grid>
<StatusChip status="pending" />
<PendingChangesChip pending={pendingChanges} accepted={changesAccepted} />
</Grid>
)}
{device.settings.needsBusControl && (

View file

@ -31,6 +31,7 @@ import LinearMutationBuilder from "common/LinearMutationBuilder";
import { makeStyles } from "@mui/styles";
import { useNavigate } from "react-router-dom";
import CancelSubmit from "common/CancelSubmit";
import { cloneDeep } from "lodash";
interface TabPanelProps {
children?: React.ReactNode;
@ -94,7 +95,6 @@ export default function DeviceSettings(props: Props) {
const { success, error } = useSnackbar();
const deviceAPI = useDeviceAPI();
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 [isRemoveDeviceDialogOpen, setIsRemoveDeviceDialogOpen] = useState<boolean>(false);
@ -103,7 +103,9 @@ export default function DeviceSettings(props: Props) {
const [compExtTwo, setCompExtTwo] = useState("");
const [compExtThree, setCompExtThree] = useState("");
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[]>>(
new Map<string, Component[]>()
);
@ -111,28 +113,22 @@ export default function DeviceSettings(props: Props) {
const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>();
useEffect(() => {
if (isDialogOpen && prevIsDialogOpen) return;
if (prevDevice !== device || isDialogOpen !== prevIsDialogOpen) {
setDeviceForm(deviceFromForm(Device.clone(props.device)));
if (device.settings.extensionComponents[0]) {
setCompExtOne(device.settings.extensionComponents[0]);
}
if (device.settings.extensionComponents[1]) {
setCompExtTwo(device.settings.extensionComponents[1]);
}
if (device.settings.extensionComponents[2]) {
setCompExtThree(device.settings.extensionComponents[2]);
}
if (device.settings.mutations) {
setLinearMutations(device.settings.mutations);
}
if (components) {
let cbd = new Map<string, Component[]>();
cbd.set(device.id().toString(), components);
setComponentsByDevice(cbd);
}
if (!isDialogOpen || prevIsDialogOpen) return;
const settingsForm = deviceFromForm(Device.clone(device));
setDeviceForm(settingsForm);
setSleeps(settingsForm.settings.sleepDurationS > 0);
setCompExtOne(settingsForm.settings.extensionComponents[0] ?? "");
setCompExtTwo(settingsForm.settings.extensionComponents[1] ?? "");
setCompExtThree(settingsForm.settings.extensionComponents[2] ?? "");
setLinearMutations(cloneDeep(settingsForm.settings.mutations));
if (components) {
let cbd = new Map<string, Component[]>();
cbd.set(settingsForm.id().toString(), components);
setComponentsByDevice(cbd);
}
}, [device, prevDevice, props.device, components, isDialogOpen, prevIsDialogOpen]);
}, [device, components, isDialogOpen, prevIsDialogOpen]);
const close = () => {
closeDialogCallback();
@ -147,7 +143,7 @@ export default function DeviceSettings(props: Props) {
const minCheckPeriodS = (): number => {
let defaultPeriod = 60;
switch (device.settings.platform) {
switch (deviceForm.settings.platform) {
case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON:
return user.hasFeature("admin") ? defaultPeriod : 300;
default:
@ -319,7 +315,7 @@ export default function DeviceSettings(props: Props) {
id="name"
name="name"
label="Name"
defaultValue={deviceForm.settings.name}
value={deviceForm.settings.name}
onChange={changeName}
margin="normal"
variant="outlined"
@ -332,7 +328,7 @@ export default function DeviceSettings(props: Props) {
id="description"
name="description"
label="Description"
defaultValue={deviceForm.settings.description}
value={deviceForm.settings.description}
onChange={changeDescription}
multiline
rows={2}
@ -550,8 +546,8 @@ export default function DeviceSettings(props: Props) {
const mutationsContent = () => {
let mutations: JSX.Element[] = [];
if (device.settings.mutations) {
device.settings.mutations.forEach((mut, i) => {
if (linearMutations) {
linearMutations.forEach((mut, i) => {
mutations.push(
<ListItem key={"mutation" + i}>
<Grid
@ -568,9 +564,7 @@ export default function DeviceSettings(props: Props) {
variant="contained"
style={{ backgroundColor: "red" }}
onClick={() => {
let lm = linearMutations;
lm.splice(i, 1);
setLinearMutations([...lm]);
setLinearMutations(linearMutations.filter((_, index) => index !== i));
}}>
Remove
</Button>
@ -696,9 +690,7 @@ export default function DeviceSettings(props: Props) {
setNewMutationDialog(false);
}}
onSubmit={newMutation => {
let lm = linearMutations;
lm.push(newMutation);
setLinearMutations(lm);
setLinearMutations([...linearMutations, newMutation]);
}}
/>
</React.Fragment>

View file

@ -75,7 +75,6 @@ export default function DeviceScannedComponents(props: Props){
let expanders: quack.AddressData[] = []
//filter the address data
let addr = scannedI2C?.settings?.foundAddresses
console.log(addr)
if(addr){
addr.forEach(addrData => {
if(!i2cBlacklistAddresses.includes(addrData.address)){
@ -87,7 +86,6 @@ export default function DeviceScannedComponents(props: Props){
}
})
}
console.log(valid)
setExpanderAddresses(expanders)
setValidI2CComponentAddresses(valid)
},[scannedI2C])
@ -213,14 +211,13 @@ export default function DeviceScannedComponents(props: Props){
})
})
}
console.log(cloneList)
setComponents(cloneList)
}
const removeScan = (key: string, type: pond.ObjectType) => {
deviceAPI.removeFoundComponents(device.settings.deviceId, key, type)
.then(resp => {
console.log("Cleared this scan")
// console.log("Cleared this scan")
}).catch(err => {
console.log("There was a problem clearing this scan")
})
@ -229,7 +226,7 @@ export default function DeviceScannedComponents(props: Props){
const removeAllScans = () => {
deviceAPI.removeAllFoundComponents(device.settings.deviceId)
.then(resp => {
console.log("Cleared all scans")
// console.log("Cleared all scans")
refreshCallback()
}).catch(err => {
console.log("There was a problem clearing scans")

View file

@ -26,6 +26,7 @@ export {
export * from "./useDebounce";
// export * from "./useForceUpdate";
// export * from "./useInterval";
export * from "./usePendingChanges";
export * from "./usePrevious";
export * from "./useThemeType";
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 {
componentIDToString,
emptyComponentId,
getComponentIDString,
sameComponentID,
stringToComponentId
} from "pbHelpers/Component";
@ -130,9 +129,6 @@ export default function InteractionSettings(props: Props) {
} = props;
const theme = useTheme();
const { success, error } = useSnackbar();
const prevInitialInteraction = usePrevious(initialInteraction);
const prevComponents = usePrevious(components);
const prevInitialComponent = usePrevious(initialComponent);
const prevIsDialogOpen = usePrevious(isDialogOpen);
const classes = useStyles();
const [{ user, as }] = useGlobalState();
@ -167,7 +163,7 @@ export default function InteractionSettings(props: Props) {
const setDefaultState = useCallback(() => {
let interaction = getDefaultInteraction();
if (initialInteraction && mode === "update") {
interaction = initialInteraction;
interaction = Interaction.clone(initialInteraction);
if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) {
setSubtypeDropdown(interaction.settings.subtype);
} else {
@ -223,7 +219,7 @@ export default function InteractionSettings(props: Props) {
setDutyCycle(interactionResult.dutyCycle.toString());
setMappedComponents(mappedComponents);
setInteraction(interaction);
}, [components, initialComponent, initialConditions, initialInteraction, mode, /*sensor*/]);
}, [components, initialComponent, initialConditions, initialInteraction, mode, sensor, user]);
const availableSources = () => {
return components.filter(component => isSource(component.settings.type));
@ -233,27 +229,12 @@ export default function InteractionSettings(props: Props) {
if (user && user.settings.timezone) {
setTimezone(user.settings.timezone);
}
if (isDialogOpen && prevIsDialogOpen) return;
if (
prevInitialInteraction !== initialInteraction ||
(prevComponents && prevComponents.length !== components.length) ||
getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) ||
isDialogOpen !== prevIsDialogOpen
) {
setDefaultState();
}
}, [
components.length,
initialComponent,
initialInteraction,
prevComponents,
prevInitialComponent,
prevInitialInteraction,
setDefaultState,
user,
isDialogOpen,
prevIsDialogOpen
]);
}, [user]);
useEffect(() => {
if (!isDialogOpen || prevIsDialogOpen) return;
setDefaultState();
}, [isDialogOpen, prevIsDialogOpen, setDefaultState]);
const getAvailableSinks = () => {
let type = or(interaction.settings.result, pond.InteractionResult.create()).type;

View file

@ -4,7 +4,6 @@ import {
CardActions,
CardContent,
CardHeader,
CircularProgress,
darken,
Grid2 as Grid,
IconButton,
@ -15,7 +14,8 @@ import {
Tooltip,
Typography
} from "@mui/material";
import { CheckCircleOutline, Settings } from "@mui/icons-material";
import { Settings } from "@mui/icons-material";
import PendingChangesIndicator from "common/PendingChangesIndicator";
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { Component, Device, Interaction } from "models";
@ -87,15 +87,6 @@ export default function InteractionsOverview(props: Props) {
undefined
);
useEffect(() => {
console.log("[interactions:mount] initial interactions:", props.interactions.map(i => ({
key: i.key(),
synced: i.status.synced,
lastUpdate: i.status.lastUpdate,
lastSynced: i.status.lastSynced,
})));
}, []);
useEffect(() => {
if (components !== prevComponents) {
let initMappedComponents: Map<string, Component> = new Map();
@ -106,17 +97,6 @@ export default function InteractionsOverview(props: Props) {
}
if (props.interactions !== prevInteractions) {
console.log("[interactions:effect] props.interactions changed, prevInteractions was", prevInteractions ? "defined" : "undefined");
props.interactions.forEach((interaction: Interaction) => {
const key = interaction.key();
const prevInteraction = prevInteractions?.find(p => p.key() === key);
console.log("[interactions:effect]", key, {
synced: interaction.status.synced,
lastUpdate: interaction.status.lastUpdate,
prevSynced: prevInteraction?.status.synced,
prevLastUpdate: prevInteraction?.status.lastUpdate,
});
});
setAcceptedInteractions(prev => {
const updated = new Set(prev);
props.interactions.forEach((interaction: Interaction) => {
@ -125,7 +105,6 @@ export default function InteractionsOverview(props: Props) {
if (!interaction.status.synced) {
updated.delete(key);
} else if (prevInteraction && !prevInteraction.status.synced) {
console.log("[interactions:effect] ACCEPTED", key);
updated.add(key);
}
});
@ -240,7 +219,6 @@ export default function InteractionsOverview(props: Props) {
interactionsAPI
.updateInteraction(Number(deviceID), settings, as)
.then((_response: any) => {
console.log("[interactions:submit] API success, setting synced=false for index", index);
let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
updatedDirtyInteractions.set(index, false);
setDirtyInteractions(updatedDirtyInteractions);
@ -249,7 +227,6 @@ export default function InteractionsOverview(props: Props) {
if (updated[index]) {
updated[index].status.synced = false;
updated[index].status.lastUpdate = moment().toISOString();
console.log("[interactions:submit] local state updated for", updated[index].key());
}
return updated;
});
@ -329,17 +306,11 @@ export default function InteractionsOverview(props: Props) {
title={interactionResultText(interaction, sink)}
subheader={
statusText ? (
<Grid container spacing={0.5} alignItems="center">
{isPending && <CircularProgress size={10} thickness={5} color="inherit" />}
{isAccepted && (
<CheckCircleOutline
sx={{ fontSize: 13, color: "var(--status-ok)" }}
/>
)}
<Grid>
{statusText}
</Grid>
</Grid>
<PendingChangesIndicator
pending={isPending}
accepted={isAccepted}
text={statusText}
/>
) : ""}
subheaderTypographyProps={{ variant: "caption" }}
action={action}

View file

@ -19,7 +19,7 @@ import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap }
import ComponentCard from "component/ComponentCard";
import { sameComponentID, sortComponents } from "pbHelpers/Component";
import { isController } from "pbHelpers/ComponentType";
import { or } from "utils";
import { getTimestampMillis, isStaleStatusUpdate, or } from "utils";
import ComponentDiagnostics from "component/ComponentDiagnostics";
import DeviceWizard from "device/DeviceWizard";
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
@ -27,12 +27,6 @@ import { useWebSocket } from "hooks/useWebSocket";
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>(
current: T | null | undefined,
incoming: T | null | undefined
@ -270,6 +264,10 @@ export default function DevicePage() {
if (!data) return;
const { key, component } = data;
setComponents((prev) => {
const existing = prev.get(key);
if (existing && isStaleStatusUpdate(existing.status, component.status)) {
return prev;
}
const updated = new Map(prev);
updated.set(key, component);
return updated;
@ -315,17 +313,7 @@ export default function DevicePage() {
return [...prev, interaction];
}
const existing = prev[index];
const existingLastUpdate = getTimestampMillis(existing.status.lastUpdate);
const incomingLastUpdate = getTimestampMillis(interaction.status.lastUpdate);
const incomingLastSynced = getTimestampMillis(interaction.status.lastSynced);
if (existingLastUpdate !== undefined && incomingLastUpdate !== undefined &&
existingLastUpdate > incomingLastUpdate) {
return prev;
}
if (!existing.status.synced && interaction.status.synced &&
incomingLastUpdate !== undefined &&
existingLastUpdate === incomingLastUpdate &&
(incomingLastSynced === undefined || incomingLastSynced < incomingLastUpdate)) {
if (isStaleStatusUpdate(existing.status, interaction.status)) {
return prev;
}
const updated = [...prev];

View file

@ -4,7 +4,7 @@ import { NextMeasurementChip } from "common/NextMeasurementChip";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
// import { getTableIcons } from "common/ResponsiveTable";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import StatusChip from "common/StatusChip";
import PendingChangesChip from "common/PendingChangesChip";
import { GetDefaultDateRange } from "common/time/DateRange";
import ComponentActions from "component/ComponentActions";
import ComponentChart from "component/ComponentChart";
@ -16,11 +16,14 @@ import {
useComponentAPI,
useDeviceAPI,
useGroupAPI,
useHTTP,
useInteractionsAPI,
usePendingChanges,
usePrevious,
useSnackbar,
useUserAPI
} from "hooks";
import { useWebSocket } from "hooks/useWebSocket";
import InteractionChip from "interactions/InteractionChip";
import InteractionSettings from "interactions/InteractionSettings";
import { cloneDeep } from "lodash";
@ -40,9 +43,10 @@ import { getDefaultInteraction } from "pbHelpers/Interaction";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
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 { useRouteMatch } from "react-router";
import { isStaleStatusUpdate } from "utils/syncStatus";
import { or } from "utils/types";
const useStyles = makeStyles((theme: Theme) => {
@ -142,6 +146,55 @@ export default function DeviceComponent() {
const [deviceComponentPrefs, setDeviceComponentPrefs] = useState<
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 = () => {
setDevice(Device.create());
@ -389,9 +442,12 @@ export default function DeviceComponent() {
alignItems="center"
wrap="nowrap"
className={classes.overviewContainer}>
{!component.status.synced && (
{(componentPending.pending || componentPending.accepted) && (
<Grid >
<StatusChip key="status" status="pending" />
<PendingChangesChip
pending={componentPending.pending}
accepted={componentPending.accepted}
/>
</Grid>
)}
<Grid >

View file

@ -534,7 +534,7 @@ export function simpleLineChartData(
}
});
}
console.log(lineData)
// console.log(lineData)
return lineData;
}

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

@ -417,7 +417,8 @@ export default function ComponentProvider(props: PropsWithChildren<Props>) {
"&end=" +
end +
(keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) +
(types ? "&types=" + types : "&types=" + ["device"])
(types ? "&types=" + types : "&types=" + ["device"]) +
(as ? "&as=" + as : "")
)
return new Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>((resolve,reject) => {
get<pond.ListComponentHistoryBetweenResponse>(url).then(resp => {

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!"
];
const DEFAULT_HEADER_COLOUR = "#1f2a35";
export interface WhiteLabelFeatures {
bins: boolean;
visualFarm: boolean;
@ -55,6 +57,7 @@ interface WhiteLabel {
secondaryColour: any;
signatureColour: any;
signatureAccentColour: any;
headerColour?: any;
auth0ClientId: string;
redirectOnLogout: boolean;
logoutRedirectTarget: string;
@ -444,6 +447,14 @@ export function getSignatureColour(): any {
return getWhitelabel().signatureColour;
}
export function getHeaderColour(): any {
return getWhitelabel().headerColour ?? DEFAULT_HEADER_COLOUR;
}
export function getHeaderColor(): any {
return getHeaderColour();
}
export function getSignatureAccentColour(): any {
return getWhitelabel().signatureAccentColour;
}

View file

@ -2,10 +2,21 @@
import * as Colours from "@mui/material/colors"
import { createTheme, ThemeOptions } from '@mui/material/styles';
import {
getHeaderColor,
getPrimaryColour,
getSecondaryColour,
} from "../services/whiteLabel";
declare module "@mui/material/styles" {
interface Palette {
header: Palette["primary"];
}
interface PaletteOptions {
header?: PaletteOptions["primary"];
}
}
const baseTheme: ThemeOptions = {
palette: {
mode: 'light', // default mode; will be overridden by system if needed
@ -62,6 +73,9 @@ export const getTheme = (mode: 'light' | 'dark') =>
...baseTheme.palette,
primary: makePaletteColor(getPrimaryColour()),
secondary: makePaletteColor(getSecondaryColour()),
header: {
main: getHeaderColor(),
},
mode,
...(mode === 'dark'
? {

View file

@ -2,5 +2,6 @@ export * from "./download";
export * from "./environment";
export * from "./numbers";
export * from "./strings";
export * from "./syncStatus";
export * from "./types";
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;
}