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", "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#staging", "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#100126cc7f3cdf18f68af6372e5db4113db012b7", "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#staging", "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

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

@ -457,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) => {
@ -485,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));
}; };
@ -832,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" &&

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

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

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,6 @@ 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 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);
@ -103,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[]>()
); );
@ -111,28 +113,22 @@ export default function DeviceSettings(props: Props) {
const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>(); const [existingMutation, setExistingMutation] = useState<pond.LinearMutation>();
useEffect(() => { useEffect(() => {
if (isDialogOpen && prevIsDialogOpen) return; if (!isDialogOpen || prevIsDialogOpen) return;
if (prevDevice !== device || isDialogOpen !== prevIsDialogOpen) {
setDeviceForm(deviceFromForm(Device.clone(props.device))); const settingsForm = deviceFromForm(Device.clone(device));
if (device.settings.extensionComponents[0]) { setDeviceForm(settingsForm);
setCompExtOne(device.settings.extensionComponents[0]); setSleeps(settingsForm.settings.sleepDurationS > 0);
} setCompExtOne(settingsForm.settings.extensionComponents[0] ?? "");
if (device.settings.extensionComponents[1]) { setCompExtTwo(settingsForm.settings.extensionComponents[1] ?? "");
setCompExtTwo(device.settings.extensionComponents[1]); setCompExtThree(settingsForm.settings.extensionComponents[2] ?? "");
} setLinearMutations(cloneDeep(settingsForm.settings.mutations));
if (device.settings.extensionComponents[2]) {
setCompExtThree(device.settings.extensionComponents[2]); if (components) {
} let cbd = new Map<string, Component[]>();
if (device.settings.mutations) { cbd.set(settingsForm.id().toString(), components);
setLinearMutations(device.settings.mutations); setComponentsByDevice(cbd);
}
if (components) {
let cbd = new Map<string, Component[]>();
cbd.set(device.id().toString(), components);
setComponentsByDevice(cbd);
}
} }
}, [device, prevDevice, props.device, components, isDialogOpen, prevIsDialogOpen]); }, [device, components, isDialogOpen, prevIsDialogOpen]);
const close = () => { const close = () => {
closeDialogCallback(); closeDialogCallback();
@ -147,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:
@ -319,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"
@ -332,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}
@ -550,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
@ -568,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>
@ -696,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

@ -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,6 @@ 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 prevComponents = usePrevious(components);
const prevInitialComponent = usePrevious(initialComponent);
const prevIsDialogOpen = usePrevious(isDialogOpen); const prevIsDialogOpen = usePrevious(isDialogOpen);
const classes = useStyles(); const classes = useStyles();
const [{ user, as }] = useGlobalState(); const [{ user, as }] = useGlobalState();
@ -167,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 {
@ -223,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));
@ -233,27 +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 (isDialogOpen && prevIsDialogOpen) return; }, [user]);
if (
prevInitialInteraction !== initialInteraction || useEffect(() => {
(prevComponents && prevComponents.length !== components.length) || if (!isDialogOpen || prevIsDialogOpen) return;
getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) || setDefaultState();
isDialogOpen !== prevIsDialogOpen }, [isDialogOpen, prevIsDialogOpen, setDefaultState]);
) {
setDefaultState();
}
}, [
components.length,
initialComponent,
initialInteraction,
prevComponents,
prevInitialComponent,
prevInitialInteraction,
setDefaultState,
user,
isDialogOpen,
prevIsDialogOpen
]);
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

@ -4,7 +4,6 @@ import {
CardActions, CardActions,
CardContent, CardContent,
CardHeader, CardHeader,
CircularProgress,
darken, darken,
Grid2 as Grid, Grid2 as Grid,
IconButton, IconButton,
@ -15,7 +14,8 @@ import {
Tooltip, Tooltip,
Typography Typography
} from "@mui/material"; } 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 { 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";
@ -87,15 +87,6 @@ export default function InteractionsOverview(props: Props) {
undefined 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(() => { useEffect(() => {
if (components !== prevComponents) { if (components !== prevComponents) {
let initMappedComponents: Map<string, Component> = new Map(); let initMappedComponents: Map<string, Component> = new Map();
@ -106,17 +97,6 @@ export default function InteractionsOverview(props: Props) {
} }
if (props.interactions !== prevInteractions) { 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 => { setAcceptedInteractions(prev => {
const updated = new Set(prev); const updated = new Set(prev);
props.interactions.forEach((interaction: Interaction) => { props.interactions.forEach((interaction: Interaction) => {
@ -125,7 +105,6 @@ export default function InteractionsOverview(props: Props) {
if (!interaction.status.synced) { if (!interaction.status.synced) {
updated.delete(key); updated.delete(key);
} else if (prevInteraction && !prevInteraction.status.synced) { } else if (prevInteraction && !prevInteraction.status.synced) {
console.log("[interactions:effect] ACCEPTED", key);
updated.add(key); updated.add(key);
} }
}); });
@ -240,7 +219,6 @@ export default function InteractionsOverview(props: Props) {
interactionsAPI interactionsAPI
.updateInteraction(Number(deviceID), settings, as) .updateInteraction(Number(deviceID), settings, as)
.then((_response: any) => { .then((_response: any) => {
console.log("[interactions:submit] API success, setting synced=false for index", index);
let updatedDirtyInteractions = cloneDeep(dirtyInteractions); let updatedDirtyInteractions = cloneDeep(dirtyInteractions);
updatedDirtyInteractions.set(index, false); updatedDirtyInteractions.set(index, false);
setDirtyInteractions(updatedDirtyInteractions); setDirtyInteractions(updatedDirtyInteractions);
@ -249,7 +227,6 @@ export default function InteractionsOverview(props: Props) {
if (updated[index]) { if (updated[index]) {
updated[index].status.synced = false; updated[index].status.synced = false;
updated[index].status.lastUpdate = moment().toISOString(); updated[index].status.lastUpdate = moment().toISOString();
console.log("[interactions:submit] local state updated for", updated[index].key());
} }
return updated; return updated;
}); });
@ -329,17 +306,11 @@ export default function InteractionsOverview(props: Props) {
title={interactionResultText(interaction, sink)} title={interactionResultText(interaction, sink)}
subheader={ subheader={
statusText ? ( statusText ? (
<Grid container spacing={0.5} alignItems="center"> <PendingChangesIndicator
{isPending && <CircularProgress size={10} thickness={5} color="inherit" />} pending={isPending}
{isAccepted && ( accepted={isAccepted}
<CheckCircleOutline text={statusText}
sx={{ fontSize: 13, color: "var(--status-ok)" }} />
/>
)}
<Grid>
{statusText}
</Grid>
</Grid>
) : ""} ) : ""}
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;
@ -315,17 +313,7 @@ export default function DevicePage() {
return [...prev, interaction]; return [...prev, interaction];
} }
const existing = prev[index]; const existing = prev[index];
const existingLastUpdate = getTimestampMillis(existing.status.lastUpdate); if (isStaleStatusUpdate(existing.status, interaction.status)) {
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)) {
return prev; return prev;
} }
const updated = [...prev]; const updated = [...prev];

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

@ -534,7 +534,7 @@ export function simpleLineChartData(
} }
}); });
} }
console.log(lineData) // console.log(lineData)
return 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=" +
end + end +
(keys ? "&keys=" + keys : "&keys=" + [deviceId.toString()]) + (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) => { return new Promise<AxiosResponse<pond.ListComponentHistoryBetweenResponse>>((resolve,reject) => {
get<pond.ListComponentHistoryBetweenResponse>(url).then(resp => { 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!" "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

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