From c5a3487db0d93e11bb5c8ee169a563d9b2bcea74 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 30 Oct 2025 16:49:00 -0600 Subject: [PATCH 01/20] added button to send down the onewire scan request --- package-lock.json | 4 +-- package.json | 2 +- src/device/DeviceWizard.tsx | 22 ++++++++++++++++ .../autoDetect/DeviceScannedComponents.tsx | 26 ++++++++++++++++--- src/device/autoDetect/ScannedOneWirePort.tsx | 7 +++++ src/models/Device.ts | 15 +++++++++++ src/pages/Device.tsx | 9 ++++--- src/providers/pond/deviceAPI.tsx | 15 +++++++++++ 8 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 src/device/autoDetect/ScannedOneWirePort.tsx diff --git a/package-lock.json b/package-lock.json index 2f6f8fc..8bf5f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,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#i2c_detect", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#onewire_detect", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -10953,7 +10953,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#0d0084ce207218ea5c8c94addfbf009814c35a1f", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#aab2a77a75ca78e22d69d4a44e1c900bbac41bcf", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index c3e3fdc..c73e616 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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#i2c_detect", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#onewire_detect", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", diff --git a/src/device/DeviceWizard.tsx b/src/device/DeviceWizard.tsx index 32387eb..6c8f361 100644 --- a/src/device/DeviceWizard.tsx +++ b/src/device/DeviceWizard.tsx @@ -141,6 +141,28 @@ export default function DeviceWizard(props: Props) { )} + {selectedPort && + // selectedPort.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY && + // device.featureSupported("detectOneWire") && + selectedPort.autoDetectable && ( + + { + setButtonClicked(true) + console.log(selectedPort) + deviceAPI.detectOneWire(device.id(), selectedPort.address) + .then(resp => { + openSnack("Device will scan port on next check-in") + }).catch(err => { + openSnack("Command failed to send to device") + }) + }}> + Scan Port + + + )} {portComponents.length > 0 && ( Current Components diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index 0381281..530ecb9 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -31,7 +31,8 @@ export default function DeviceScannedComponents(props: Props){ const compAPI = useComponentAPI(); const deviceAPI = useDeviceAPI() const [scannedI2C, setScannedI2C] = useState() //the unmodified scan of addresses - const [validCompAddresses, setValidComponentAddresses] = useState([]) //the filtered array of address data + const [scannedOneWire, setScannedOneWire] = useState([]) + const [validI2CCompAddresses, setValidI2CComponentAddresses] = useState([]) //the filtered array of address data const [components, setComponents] = useState([]) const [steps, setSteps] = useState([]); const { error, success } = useSnackbar(); @@ -40,8 +41,8 @@ export default function DeviceScannedComponents(props: Props){ const [openDialog, setOpenDialog] = useState(false); useEffect(()=>{ - //console.log(scannedComponents) if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c) + if (scannedComponents.oneWire) setScannedOneWire(scannedComponents.oneWire) //makes the array empty for testing // if (scannedComponents.i2c) { // let clone = cloneDeep(scannedComponents.i2c) @@ -76,7 +77,7 @@ export default function DeviceScannedComponents(props: Props){ } }) } - setValidComponentAddresses(valid) + setValidI2CComponentAddresses(valid) },[scannedI2C]) const stepper = () => { @@ -225,7 +226,7 @@ export default function DeviceScannedComponents(props: Props){ - {validCompAddresses.length > 0 ? validCompAddresses.map((addr, index) => { + {validI2CCompAddresses.length > 0 ? validI2CCompAddresses.map((addr, index) => { return ( ()}/> ) @@ -242,6 +243,23 @@ export default function DeviceScannedComponents(props: Props){ } } + {scannedOneWire.length > 0 && + + + + Pin Port Sensors + + + + {scannedOneWire.map(scan => { + return( + + + + ) + })} + + } diff --git a/src/device/autoDetect/ScannedOneWirePort.tsx b/src/device/autoDetect/ScannedOneWirePort.tsx new file mode 100644 index 0000000..f391dd3 --- /dev/null +++ b/src/device/autoDetect/ScannedOneWirePort.tsx @@ -0,0 +1,7 @@ +import React from "react" + +export default function ScannedOneWirePort(){ + return ( + + ) +} \ No newline at end of file diff --git a/src/models/Device.ts b/src/models/Device.ts index 0aec35a..29f06ef 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -48,6 +48,21 @@ const featureVersions: Map = new Map([ v2CellBlue: "2.1.7", v2EthBlue: "2.1.7" } + ],[ + "detectOneWire", + { + photon: "N/A", + electron: "N/A", + v2Wifi: "N/A", + v2Cell: "N/A", + v2WifiS3: "N/A", + v2CellS3: "N/A", + v2CellBlack: "2.1.8", + v2CellGreen: "N/A", + v2WifiBlue: "2.1.8", + v2CellBlue: "2.1.8", + v2EthBlue: "2.1.8" + } ] ]); export class Device { diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 4d7d41f..e9012ee 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -1,4 +1,4 @@ -import { Button, Card, Divider, List, ListItem, Typography } from "@mui/material"; +import { Box, Button, Card, Divider, List, ListItem, Typography } from "@mui/material"; import Grid from '@mui/material/Grid2'; import { Component, Device, Interaction, User } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; @@ -169,7 +169,8 @@ export default function DevicePage() { const loadPortScan = () => { deviceAPI.listFoundComponents(parseInt(deviceID)) .then(resp => { - if (resp.data.foundComponents?.i2c){ + console.log(resp.data) + if (resp.data.foundComponents){ setScannedAddresses(resp.data.foundComponents ?? undefined) } }) @@ -430,8 +431,8 @@ export default function DevicePage() { refreshCallback={loadDevice} /> - {scannedAddresses && - + {(scannedAddresses?.i2c || scannedAddresses?.oneWire) && + Promise>; getMulti: (ids: number[] | string[], otherTeam?: string) => Promise>; detectI2C: (id: number, otherTeam?: string) => Promise>; + detectOneWire: (id: number, port: number, otherTeam?: string) => Promise> listFoundComponents: (id: number, otherTeam?: string) => Promise> getGeoJson: (id: number | string, demo?: boolean, otherTeam?: string) => Promise; getMultiGeoJson: (ids: number[] | string[], otherTeam?: string) => Promise; @@ -962,6 +963,19 @@ export default function DeviceProvider(props: PropsWithChildren) { }) } + const detectOneWire = (id: number, port: number, otherTeam?: string) => { + let url = "/devices/" + id + "/detectOneWire?port=" + port; + const view = otherTeam ? otherTeam : as + if(view) url = url + "?as=" + view + return new Promise>((resolve, reject) => { + put(pondURL(url)).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + const listFoundComponents = (id: number, otherTeam?: string) => { let url = "/devices/" + id + "/listScannedComponents"; const view = otherTeam ? otherTeam : as @@ -1035,6 +1049,7 @@ export default function DeviceProvider(props: PropsWithChildren) { updateComponentPreferences, listDeviceComponentPreferences, detectI2C, + detectOneWire, listFoundComponents, removeFoundComponents }}> From 8f601b360e26f14eb5aa97ebc9b4b062619d9ee8 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 3 Nov 2025 09:22:47 -0600 Subject: [PATCH 02/20] put in all the data that will be displayed --- package-lock.json | 2 +- .../autoDetect/DeviceScannedComponents.tsx | 21 +++++++-- .../autoDetect/OneWire/PortComponent.tsx | 47 +++++++++++++++++++ .../autoDetect/OneWire/ScannedOneWirePort.tsx | 31 ++++++++++++ src/device/autoDetect/ScannedOneWirePort.tsx | 7 --- src/pages/Device.tsx | 2 +- 6 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/device/autoDetect/OneWire/PortComponent.tsx create mode 100644 src/device/autoDetect/OneWire/ScannedOneWirePort.tsx delete mode 100644 src/device/autoDetect/ScannedOneWirePort.tsx diff --git a/package-lock.json b/package-lock.json index 8bf5f65..04f2a51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10953,7 +10953,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#aab2a77a75ca78e22d69d4a44e1c900bbac41bcf", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#53e313b9d0a3b0c081be17f9a599abed80259c6c", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index 530ecb9..18ea15f 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -7,9 +7,11 @@ import { useEffect, useState } from "react"; import ScannedI2C from "./ScannedI2C"; import ComponentForm from "component/ComponentForm"; import { Component, Device } from "models"; -import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability"; +import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability"; import ResponsiveDialog from "common/ResponsiveDialog"; import { useComponentAPI, useDeviceAPI, useSnackbar } from "hooks"; +import { ConfigurablePin } from "pbHelpers/AddressTypes"; +import ScannedOneWirePort from "./OneWire/ScannedOneWirePort"; interface Props { scannedComponents: pond.ComponentAddressMap @@ -251,10 +253,21 @@ export default function DeviceScannedComponents(props: Props){ - {scannedOneWire.map(scan => { + {scannedOneWire.map((scan,i) => { + let map = FindAvailablePositions([], device.settings.product).availability.get(quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY) + let portAddress = scan.settings?.oneWireData?.port + let portLabel = "" + + map?.forEach(entry => { + let pin = entry as ConfigurablePin + if (pin.address && pin.address === portAddress) { + portLabel = pin.label + } + }) return( - - + + Port: {portLabel} + ) })} diff --git a/src/device/autoDetect/OneWire/PortComponent.tsx b/src/device/autoDetect/OneWire/PortComponent.tsx new file mode 100644 index 0000000..c92dfac --- /dev/null +++ b/src/device/autoDetect/OneWire/PortComponent.tsx @@ -0,0 +1,47 @@ +import { Box, MenuItem, TextField } from "@mui/material" +import { getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType" +import { quack } from "protobuf-ts/quack" +import React, { useEffect, useState } from "react" + +interface Props { + compData: quack.OneWireComponentData +} + +export default function PortComponent(props: Props){ + const {compData} = props + const [selectedSubtype, setSelectedSubtype] = useState(-1) + + useEffect(()=>{ + setSelectedSubtype(compData.subtype) + },[compData]) + + const subtypeSelector = (compData: quack.OneWireComponentData) => { + let validSubtypes: Subtype[] = [] + getSubtypes(compData.type).forEach(option => { + if(compData.subtype === option.key || compData.subtype === 0){ + validSubtypes.push(option) + } + }) + return ( + + Select the Component Subtype + {validSubtypes.map((s, i) => ( + {s.friendlyName} + ))} + + ) + } + + return ( + + + {getFriendlyName(compData.type)} + Cable ID: {compData.cableId} + Number of nodes: {compData.nodeCount} + {subtypeSelector(compData)} + + + ) +} \ No newline at end of file diff --git a/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx b/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx new file mode 100644 index 0000000..a4f3e11 --- /dev/null +++ b/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx @@ -0,0 +1,31 @@ +import { Box, MenuItem, TextField } from "@mui/material" +import { extension, getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType" +import { pond } from "protobuf-ts/pond" +import { quack } from "protobuf-ts/quack" +import React, { useEffect, useState } from "react" +import PortComponent from "./PortComponent" + +interface Props { + scan: pond.DetectOneWire +} + +export default function ScannedOneWirePort(props: Props){ + const {scan} = props + const [portComponents, setPortComponents] = useState([]) + + useEffect(()=>{ + setPortComponents(scan.settings?.oneWireData?.components ?? []) + },[scan]) + + return ( + + {portComponents.map((compData, i) => { + // let ext = extension(compData.type, compData.subtype) + // console.log(ext) + return ( + + ) + })} + + ) +} \ No newline at end of file diff --git a/src/device/autoDetect/ScannedOneWirePort.tsx b/src/device/autoDetect/ScannedOneWirePort.tsx deleted file mode 100644 index f391dd3..0000000 --- a/src/device/autoDetect/ScannedOneWirePort.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react" - -export default function ScannedOneWirePort(){ - return ( - - ) -} \ No newline at end of file diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index e9012ee..ec3443d 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -171,7 +171,7 @@ export default function DevicePage() { .then(resp => { console.log(resp.data) if (resp.data.foundComponents){ - setScannedAddresses(resp.data.foundComponents ?? undefined) + setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined) } }) .catch(err => { From 65cb7acbab85037fc39d8e016071dfc082d46faa Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 3 Nov 2025 13:23:02 -0600 Subject: [PATCH 03/20] finished the functionality for adding components from the scan --- .../autoDetect/DeviceScannedComponents.tsx | 13 ++-- .../autoDetect/OneWire/PortComponent.tsx | 62 +++++++++++++++---- .../autoDetect/OneWire/ScannedOneWirePort.tsx | 31 ++++++++-- src/pbHelpers/ComponentType.tsx | 1 + .../ComponentTypes/CapacitorCable.ts | 1 + src/pbHelpers/ComponentTypes/GrainCable.ts | 1 + src/pbHelpers/ComponentTypes/PressureCable.ts | 1 + 7 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index 18ea15f..e1fc020 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -203,6 +203,7 @@ export default function DeviceScannedComponents(props: Props){ }) }) } + console.log(cloneList) setComponents(cloneList) } @@ -251,7 +252,6 @@ export default function DeviceScannedComponents(props: Props){ Pin Port Sensors - {scannedOneWire.map((scan,i) => { let map = FindAvailablePositions([], device.settings.product).availability.get(quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY) @@ -265,9 +265,14 @@ export default function DeviceScannedComponents(props: Props){ } }) return( - - Port: {portLabel} - + + + Port: {portLabel} + + + ) })} diff --git a/src/device/autoDetect/OneWire/PortComponent.tsx b/src/device/autoDetect/OneWire/PortComponent.tsx index c92dfac..ee2ab3f 100644 --- a/src/device/autoDetect/OneWire/PortComponent.tsx +++ b/src/device/autoDetect/OneWire/PortComponent.tsx @@ -1,46 +1,84 @@ -import { Box, MenuItem, TextField } from "@mui/material" +import { Box, Checkbox, MenuItem, TextField, Tooltip } from "@mui/material" import { getFriendlyName, getSubtypes, Subtype } from "pbHelpers/ComponentType" import { quack } from "protobuf-ts/quack" import React, { useEffect, useState } from "react" interface Props { compData: quack.OneWireComponentData + componentSelect: (selected: boolean, componentData: quack.OneWireComponentData) => void } export default function PortComponent(props: Props){ - const {compData} = props - const [selectedSubtype, setSelectedSubtype] = useState(-1) + const {compData, componentSelect} = props + // const [selectedSubtype, setSelectedSubtype] = useState(0) + const [subtypeVal, setSubtypeVal] = useState("none")//note that this is the friendly name and not the key or the value because it needs to be different from the other alias options + const [subtypeMap, setSubtypeMap] = useState>(new Map()) + const [subtypeOptions, setSubtypeOptions] = useState([]) + const [added, setAdded] = useState(false) useEffect(()=>{ - setSelectedSubtype(compData.subtype) - },[compData]) - - const subtypeSelector = (compData: quack.OneWireComponentData) => { + // setSelectedSubtype(compData.subtype) let validSubtypes: Subtype[] = [] + let subMap: Map = new Map() + let subVal: string = "none" getSubtypes(compData.type).forEach(option => { if(compData.subtype === option.key || compData.subtype === 0){ validSubtypes.push(option) + if(subVal === "none"){ + subVal = option.friendlyName + } } + subMap.set(option.friendlyName, option.key) }) + setSubtypeOptions(validSubtypes) + setSubtypeMap(subMap) + setSubtypeVal(subVal) + },[compData]) + + const subtypeSelector = () => { return ( { + setSubtypeVal(event.target.value) + // setSelectedSubtype(subtypeMap.get(event.target.value) ?? 0) + compData.subtype = subtypeMap.get(event.target.value) ?? 0 + }} select> - Select the Component Subtype - {validSubtypes.map((s, i) => ( - {s.friendlyName} + Select the Component Subtype + {subtypeOptions.map((s, i) => ( + {s.friendlyName} ))} ) } + const selectCompCheckbox = () => { + return ( + { + setAdded(checked) + componentSelect(checked, compData) + }} + /> + } + /> + ) + } + return ( {getFriendlyName(compData.type)} Cable ID: {compData.cableId} Number of nodes: {compData.nodeCount} - {subtypeSelector(compData)} + {subtypeOptions.length < 2 ? subtypeVal : subtypeSelector()} + {selectCompCheckbox()} ) diff --git a/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx b/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx index a4f3e11..4fd99d8 100644 --- a/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx +++ b/src/device/autoDetect/OneWire/ScannedOneWirePort.tsx @@ -4,26 +4,49 @@ import { pond } from "protobuf-ts/pond" import { quack } from "protobuf-ts/quack" import React, { useEffect, useState } from "react" import PortComponent from "./PortComponent" +import { Component } from "models" interface Props { scan: pond.DetectOneWire + componentSelectionCallback: (newComponents: Component[], checked: boolean) => void } export default function ScannedOneWirePort(props: Props){ - const {scan} = props + const {scan, componentSelectionCallback} = props const [portComponents, setPortComponents] = useState([]) useEffect(()=>{ setPortComponents(scan.settings?.oneWireData?.components ?? []) },[scan]) + const componentSelected = (checked: boolean, componentData: quack.OneWireComponentData) => { + let newComponents: Component[] = [] + //use the component data to create a new Component + //build a component with given information and defaults for the rest + let primaryComponent = Component.create() + primaryComponent.settings.type = componentData.type + primaryComponent.settings.subtype = componentData.subtype + primaryComponent.settings.address = scan.settings?.oneWireData?.port ?? 0 + primaryComponent.settings.addressType = quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY + primaryComponent.settings.name = getFriendlyName(componentData.type, componentData.subtype) + + //need to check if the component is chainable, meaning the address type will reflect the cable id + let ext = extension(componentData.type, componentData.subtype) + if(ext.isChainable){ + primaryComponent.settings.addressType = componentData.cableId + 8 + } + newComponents.push(primaryComponent) + + componentSelectionCallback(newComponents, checked) + } + return ( {portComponents.map((compData, i) => { - // let ext = extension(compData.type, compData.subtype) - // console.log(ext) return ( - + { + componentSelected(checked, component) + }}/> ) })} diff --git a/src/pbHelpers/ComponentType.tsx b/src/pbHelpers/ComponentType.tsx index 60f14cd..03be638 100644 --- a/src/pbHelpers/ComponentType.tsx +++ b/src/pbHelpers/ComponentType.tsx @@ -168,6 +168,7 @@ export interface ComponentTypeExtension { //if the map does not exist then just use the array from the device availability, //if the map does exist filter the availability after claims to find matching address between them and use that subtypeI2CMap?: Map + isChainable?: boolean } export interface Summary { diff --git a/src/pbHelpers/ComponentTypes/CapacitorCable.ts b/src/pbHelpers/ComponentTypes/CapacitorCable.ts index cc56e27..8843952 100644 --- a/src/pbHelpers/ComponentTypes/CapacitorCable.ts +++ b/src/pbHelpers/ComponentTypes/CapacitorCable.ts @@ -68,6 +68,7 @@ export function CapacitorCable(subtype: number = 0): ComponentTypeExtension { isSource: true, isArray: true, isCalibratable: false, + isChainable: true, addressTypes: addressTypes, interactionResultTypes: [], states: [], diff --git a/src/pbHelpers/ComponentTypes/GrainCable.ts b/src/pbHelpers/ComponentTypes/GrainCable.ts index c715611..bacc514 100644 --- a/src/pbHelpers/ComponentTypes/GrainCable.ts +++ b/src/pbHelpers/ComponentTypes/GrainCable.ts @@ -176,6 +176,7 @@ export function GrainCable(subtype: number = 0): ComponentTypeExtension { isSource: true, isArray: true, isCalibratable: false, + isChainable: true, addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, quack.AddressType.ADDRESS_TYPE_I2C], interactionResultTypes: [], states: [], diff --git a/src/pbHelpers/ComponentTypes/PressureCable.ts b/src/pbHelpers/ComponentTypes/PressureCable.ts index f03cc9e..3a22362 100644 --- a/src/pbHelpers/ComponentTypes/PressureCable.ts +++ b/src/pbHelpers/ComponentTypes/PressureCable.ts @@ -64,6 +64,7 @@ export function PressureCable(subtype: number = 0): ComponentTypeExtension { isArray: true, hasFan: true, isCalibratable: false, + isChainable: true, addressTypes: [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY], interactionResultTypes: [], states: [], From 487c8617e4fe2ba2d514a2f76c3264a18cfb8600 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 3 Nov 2025 16:41:26 -0600 Subject: [PATCH 04/20] first iteration of basic layout --- src/device/DeviceWizard.tsx | 4 +- .../autoDetect/DeviceScannedComponents.tsx | 10 +-- .../autoDetect/OneWire/PortComponent.tsx | 89 ++++++++++++++++--- .../autoDetect/OneWire/ScannedOneWirePort.tsx | 9 +- src/pages/Device.tsx | 11 ++- 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/device/DeviceWizard.tsx b/src/device/DeviceWizard.tsx index 6c8f361..cf0a061 100644 --- a/src/device/DeviceWizard.tsx +++ b/src/device/DeviceWizard.tsx @@ -142,8 +142,8 @@ export default function DeviceWizard(props: Props) { )} {selectedPort && - // selectedPort.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY && - // device.featureSupported("detectOneWire") && + selectedPort.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY && + device.featureSupported("detectOneWire") && selectedPort.autoDetectable && ( {scannedI2C !== undefined && - + I2C Sensors @@ -244,10 +244,10 @@ export default function DeviceScannedComponents(props: Props){ } - + } {scannedOneWire.length > 0 && - + Pin Port Sensors @@ -265,8 +265,8 @@ export default function DeviceScannedComponents(props: Props){ } }) return( - - + + Port: {portLabel} + {validI2CCompAddresses.length > 0 ? validI2CCompAddresses.map((addr, index) => { return ( @@ -269,7 +269,7 @@ export default function DeviceScannedComponents(props: Props){ Port: {portLabel} diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index f967774..56a0c0f 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -149,7 +149,7 @@ export interface IDeviceAPIContext { keys?: string[], types?: string[] ) => Promise; - removeFoundComponents: (id: number, key: string, otherTeam?: string) => Promise>; + removeFoundComponents: (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => Promise>; } export const DeviceAPIContext = createContext({} as IDeviceAPIContext); @@ -989,7 +989,7 @@ export default function DeviceProvider(props: PropsWithChildren) { }) } - const removeFoundComponents = (id: number, key: string, otherTeam?: string) => { + const removeFoundComponents = (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => { let url = "/devices/" + id + "/removeFoundComponents/" + key; const view = otherTeam ? otherTeam : as if(view) url = url + "?as=" + view From 8dcc0405581c9d359a5be695bbc8a88df39b6ca9 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 4 Nov 2025 12:28:21 -0600 Subject: [PATCH 06/20] fixing the url for the api call --- src/providers/pond/deviceAPI.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index 56a0c0f..391caf7 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -990,9 +990,9 @@ export default function DeviceProvider(props: PropsWithChildren) { } const removeFoundComponents = (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => { - let url = "/devices/" + id + "/removeFoundComponents/" + key; + let url = "/devices/" + id + "/removeFoundComponents/" + key + "?type=" + type; const view = otherTeam ? otherTeam : as - if(view) url = url + "?as=" + view + if(view) url = url + "&as=" + view return new Promise>((resolve, reject) => { del(pondURL(url)).then(resp => { return resolve(resp) From efb3c9eb59d5413de77f7b366dc95bbd6a9fa91b Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 4 Nov 2025 12:29:08 -0600 Subject: [PATCH 07/20] use scanType instead of type --- src/providers/pond/deviceAPI.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index 391caf7..4a90b85 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -990,7 +990,7 @@ export default function DeviceProvider(props: PropsWithChildren) { } const removeFoundComponents = (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => { - let url = "/devices/" + id + "/removeFoundComponents/" + key + "?type=" + type; + let url = "/devices/" + id + "/removeFoundComponents/" + key + "?scanType=" + type; const view = otherTeam ? otherTeam : as if(view) url = url + "&as=" + view return new Promise>((resolve, reject) => { From 14126fd7c5e60b41562d5cfbe57ada0a00f9138f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 4 Nov 2025 14:16:40 -0600 Subject: [PATCH 08/20] adding button for clearing all of the scans, including ones that were replaced by new scans --- package-lock.json | 2 +- .../autoDetect/DeviceScannedComponents.tsx | 28 +++++++++++++++++++ src/pages/Device.tsx | 2 +- src/providers/pond/deviceAPI.tsx | 19 +++++++++++-- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f60c304..1bef6b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10953,7 +10953,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#9dcc83dddad31d4a4d8a002ec31475cbb83f0689", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#910e6c4dd322a52a7f0c82d5bde650d8ccfff548", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index 3448b86..cda95ae 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -41,6 +41,7 @@ export default function DeviceScannedComponents(props: Props){ const [currentStep, setCurrentStep] = useState(0) const [settingsValid, setSettingsValid] = useState(false); const [openDialog, setOpenDialog] = useState(false); + const [clearOpen, setClearOpen] = useState(false); useEffect(()=>{ if (scannedComponents.i2c) setScannedI2C(scannedComponents.i2c) @@ -216,11 +217,38 @@ export default function DeviceScannedComponents(props: Props){ }) } + const removeAllScans = () => { + deviceAPI.removeAllFoundComponents(device.settings.deviceId) + .then(resp => { + console.log("Cleared all scans") + }).catch(err => { + console.log("There was a problem clearing scans") + }) + } + + const clearWarning = () => { + return ( + {setClearOpen(false)}}> + Clear All Scans + This action will clear all scans for this device. + + + + + + ) + } + return ( + {clearWarning()} + + Sensor Scan + + {scannedI2C !== undefined && diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 0a4e89a..7344659 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -170,7 +170,7 @@ export default function DevicePage() { deviceAPI.listFoundComponents(parseInt(deviceID)) .then(resp => { console.log(resp.data) - if (resp.data.foundComponents){ + if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){ setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined) } }) diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index 4a90b85..241781b 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -149,6 +149,7 @@ export interface IDeviceAPIContext { keys?: string[], types?: string[] ) => Promise; + removeAllFoundComponents: (id: number,otherTeam?: string) => Promise>; removeFoundComponents: (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => Promise>; } @@ -990,7 +991,7 @@ export default function DeviceProvider(props: PropsWithChildren) { } const removeFoundComponents = (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => { - let url = "/devices/" + id + "/removeFoundComponents/" + key + "?scanType=" + type; + let url = "/devices/" + id + "/removeFoundComponents/" + key + "?scanType=" + type; const view = otherTeam ? otherTeam : as if(view) url = url + "&as=" + view return new Promise>((resolve, reject) => { @@ -1002,6 +1003,19 @@ export default function DeviceProvider(props: PropsWithChildren) { }) } + const removeAllFoundComponents = (id: number, otherTeam?: string) => { + let url = "/devices/" + id + "/removeAllFoundComponents"; + const view = otherTeam ? otherTeam : as + if(view) url = url + "?as=" + view + return new Promise>((resolve, reject) => { + del(pondURL(url)).then(resp => { + return resolve(resp) + }).catch(err => { + return reject(err) + }) + }) + } + return ( ) { detectI2C, detectOneWire, listFoundComponents, - removeFoundComponents + removeFoundComponents, + removeAllFoundComponents }}> {children} From 4e60d9bcdb2044078e1254b9a0b1bc46b14dbce4 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Tue, 4 Nov 2025 15:13:46 -0600 Subject: [PATCH 09/20] closing the dialog and using the refresh callback --- src/device/autoDetect/DeviceScannedComponents.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index cda95ae..2be89a9 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -221,6 +221,7 @@ export default function DeviceScannedComponents(props: Props){ deviceAPI.removeAllFoundComponents(device.settings.deviceId) .then(resp => { console.log("Cleared all scans") + refreshCallback() }).catch(err => { console.log("There was a problem clearing scans") }) @@ -233,7 +234,10 @@ export default function DeviceScannedComponents(props: Props){ This action will clear all scans for this device. - + ) From fa85c3b6aba3980d4b3ef3daa8ad10fb7779a979 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 13 Nov 2025 11:35:31 -0600 Subject: [PATCH 10/20] increase the feature version --- src/models/Device.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/Device.ts b/src/models/Device.ts index 29f06ef..de919e4 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -57,11 +57,11 @@ const featureVersions: Map = new Map([ v2Cell: "N/A", v2WifiS3: "N/A", v2CellS3: "N/A", - v2CellBlack: "2.1.8", + v2CellBlack: "2.1.9", v2CellGreen: "N/A", - v2WifiBlue: "2.1.8", - v2CellBlue: "2.1.8", - v2EthBlue: "2.1.8" + v2WifiBlue: "2.1.9", + v2CellBlue: "2.1.9", + v2EthBlue: "2.1.9" } ] ]); From 895554786830d40e6768a9b541d2106842aae7bd Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 20 Nov 2025 10:28:08 -0600 Subject: [PATCH 11/20] increasing the detect onewire to 10 since 9 is going to be for increasing the conditions on an interaction --- src/models/Device.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/Device.ts b/src/models/Device.ts index de919e4..0851041 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -57,11 +57,11 @@ const featureVersions: Map = new Map([ v2Cell: "N/A", v2WifiS3: "N/A", v2CellS3: "N/A", - v2CellBlack: "2.1.9", + v2CellBlack: "2.1.10", v2CellGreen: "N/A", - v2WifiBlue: "2.1.9", - v2CellBlue: "2.1.9", - v2EthBlue: "2.1.9" + v2WifiBlue: "2.1.10", + v2CellBlue: "2.1.10", + v2EthBlue: "2.1.10" } ] ]); From 3cdddb928f9c36eca3db2a701013e928bc555314 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 26 Nov 2025 16:50:09 -0600 Subject: [PATCH 12/20] refactoring object interactions into multiple components and taking into account controller interactions having to be on the same device where the alerts dont matter --- src/models/Interaction.ts | 13 ++ src/objects/objectInteractions/Alerts.tsx | 74 +++++++ src/objects/objectInteractions/Controls.tsx | 61 ++++++ .../NewObjectInteraction.tsx | 94 ++++++++ .../objectInteractions/Notifications.tsx | 0 .../objectInteractions/ObjectInteractions.tsx | 204 ++++++++++++++++++ src/pages/Bin.tsx | 15 +- 7 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 src/objects/objectInteractions/Alerts.tsx create mode 100644 src/objects/objectInteractions/Controls.tsx create mode 100644 src/objects/objectInteractions/NewObjectInteraction.tsx create mode 100644 src/objects/objectInteractions/Notifications.tsx create mode 100644 src/objects/objectInteractions/ObjectInteractions.tsx diff --git a/src/models/Interaction.ts b/src/models/Interaction.ts index 659ca70..f9edba9 100644 --- a/src/models/Interaction.ts +++ b/src/models/Interaction.ts @@ -1,6 +1,7 @@ import { pond } from "protobuf-ts/pond"; import { or } from "utils/types"; import { cloneDeep } from "lodash"; +import { Result } from "pbHelpers/Enums"; export class Interaction { public settings: pond.InteractionSettings = pond.InteractionSettings.create(); @@ -53,6 +54,18 @@ export class Interaction { return subtype; } + //these two are not mutually exclusive, it is possible to be an alert and a control + public isAlert(): boolean { + if(this.settings.result?.type === Result.report) return true + if(this.settings.notifications?.reports && this.settings.notifications.notify) return true + return false + } + + public isControl(): boolean { + if(this.settings.result?.type !== Result.report) return true //as of right now all interactions that are not report are some sort of control + return false + } + public static upToSubtype(nodeNumber: number): number { let subtype = 0; switch (nodeNumber) { diff --git a/src/objects/objectInteractions/Alerts.tsx b/src/objects/objectInteractions/Alerts.tsx new file mode 100644 index 0000000..0c1ab01 --- /dev/null +++ b/src/objects/objectInteractions/Alerts.tsx @@ -0,0 +1,74 @@ +import { ExpandMore } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, List, ListItem, ListSubheader, Typography } from "@mui/material"; +import { Component } from "models"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { pond, quack } from "protobuf-ts/pond"; +import React, { useState } from "react"; +import NewObjectInteraction from "./NewObjectInteraction"; + +export interface Alert { + conditions: pond.InteractionCondition[]; + sourceComponents: Component[]; +} + +interface Props { + alerts: Alert[] +} + +export default function Alerts(props: Props){ + const {alerts} = props + const [openNewAlert, setOpenNewAlert] = useState(false) + + const readableCondition = (condition: pond.InteractionCondition) => { + let describer = describeMeasurement(condition.measurementType); + let type = describer.label(); + let comparison = + condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO + ? "Exactly" + : condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN + ? "Above" + : "Below"; + let value = describer.toDisplay(condition.value); + return ( + + {type + " " + comparison + " " + value + describer.GetUnit()} + + ); + }; + + const alertAccordion = (alert: Alert) => { + return ( + + }> + + {alert.conditions.map((condition, i) => ( + + {readableCondition(condition)} + + ))} + + + + + Reporting Components + {alert.sourceComponents.map((comp, i) => ( + {comp.name()} + ))} + + + + ); + }; + + return ( + + {}}/> + + Alerts + {alerts.map((alert, i) => ( + {alertAccordion(alert)} + ))} + + + ); +} \ No newline at end of file diff --git a/src/objects/objectInteractions/Controls.tsx b/src/objects/objectInteractions/Controls.tsx new file mode 100644 index 0000000..096c9ca --- /dev/null +++ b/src/objects/objectInteractions/Controls.tsx @@ -0,0 +1,61 @@ +import { Accordion, AccordionDetails, AccordionSummary, List, ListItem, ListSubheader } from "@mui/material"; +import { Component, Device } from "models"; +import { pond } from "protobuf-ts/pond"; +import React, { useEffect, useState } from "react"; + +export interface Control { + conditions: pond.InteractionCondition[]; + sourceComponents: Component[]; + result: pond.InteractionResult | null; + sinkComponent: Component + device: Device +} + +interface Props { + controls: Control[] +} + +export default function Controls(props: Props){ + const { controls } = props + const [deviceControls, setDeviceControls] = useState>(new Map()) + const [deviceNameMap, setDeviceNameMap] = useState>(new Map()) + + useEffect(()=>{ + let map: Map = new Map() + let nameMap: Map = new Map() + controls.forEach(control => { + let mappedControls = map.get(control.device.id()) + if(mappedControls){ + mappedControls.push(control) + }else{ + map.set(control.device.id(), [control]) + } + if(!nameMap.get(control.device.id())){ + nameMap.set(control.device.id(), control.device.name()) + } + }) + setDeviceControls(map) + setDeviceNameMap(nameMap) + },[controls]) + + const controlAccordion = (control: Control) => { + return ( + + {control.device.name()} + + + ) + } + + + return ( + + + Controls + {controls.map((control, i) => ( + {controlAccordion(control)} + ))} + + + ) +} \ No newline at end of file diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx new file mode 100644 index 0000000..31170e3 --- /dev/null +++ b/src/objects/objectInteractions/NewObjectInteraction.tsx @@ -0,0 +1,94 @@ +import { Button, DialogActions, DialogContent, DialogTitle, MenuItem, Select } from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { getMeasurements } from "pbHelpers/ComponentType"; +import { Measurement, Operator } from "pbHelpers/Enums"; +import { pond } from "protobuf-ts/pond"; +import { quack } from "protobuf-ts/quack"; +import { useEffect, useState } from "react"; + +interface Props { + open: boolean + onClose: (refreshCallback: boolean) => void + //if we pass in a device id we could filter the components to only be for that device, this could be used for the control part + device?: number +} + +export default function NewObjectInteraction(props: Props){ + const { open, onClose } = props + const [newAlertComponentType, setNewAlertComponentType] = useState( + quack.ComponentType.COMPONENT_TYPE_INVALID + ); + const [conditions, setConditions] = useState([]); + const [selectedAlertComponents, setSelectedAlertComponents] = useState([]); + + useEffect(() => {},[]) + + + const close = (refresh: boolean) => { + onClose(refresh) + } + const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => { + return getMeasurements(type).map(m => m.measurementType); + }; + + const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => { + let measurementType = availableMeasurementTypes(type)[0]; + let condition = pond.InteractionCondition.create({ + measurementType: measurementType, + comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less, + value: 0 + }); + return [condition]; + }; + + return ( + { + close(false); + }}> + Set New Alert + + {/* Dropdown to select the component type to add the interaction to */} + + {/* list of checkboxes for the components that match that type */} + {listMatchingComponents()} + {/* have the conditions set here */} + {conditionInput()} + {/* have the schedule set here */} + {scheduler()} + + + + + + + ) +} \ No newline at end of file diff --git a/src/objects/objectInteractions/Notifications.tsx b/src/objects/objectInteractions/Notifications.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx new file mode 100644 index 0000000..aa9f269 --- /dev/null +++ b/src/objects/objectInteractions/ObjectInteractions.tsx @@ -0,0 +1,204 @@ +import { Box, Button, Card, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; +import Alerts, { Alert } from "./Alerts"; +import Controls, { Control } from "./Controls"; +import { Component, Device, Interaction } from "models"; +import { quack } from "protobuf-ts/quack"; +import { Option } from "common/SearchSelect"; +import { useInteractionsAPI } from "hooks"; +import { useGlobalState } from "providers"; +import { extension, getFriendlyName } from "pbHelpers/ComponentType"; +import { sameComponentID } from "pbHelpers/Component"; + +interface Props { + linkedComponents: Map; //the component key to the component object + componentDevices: Map; + devices: Device[]; +} + +export default function ObjectInteractions(props: Props) { + const { linkedComponents, componentDevices, devices } = props + const [{as}] = useGlobalState() + //list of alerts, each alert contains a list of interactions with matching conditions + const [alerts, setAlerts] = useState([]); + //list of interactions relating to a controller + const [controls, setControls] = useState([]); + const interactionsAPI = useInteractionsAPI(); + const [sinkOptions, setSinkOptions] = useState([]) + const [typeOptions, setTypeOptions] = useState([]); + + + const matchConditions = (interaction: Interaction, set: Alert | Control) => { + //if the number of conditions are not the same they are different + if (interaction.settings.conditions.length !== set.conditions.length) { + return false; + } + //continue with the comparison + let matching = true; + interaction.settings.conditions.forEach((condition, i) => { + if ( + condition.comparison !== set.conditions[i].comparison || + condition.measurementType !== set.conditions[i].measurementType || + condition.value !== set.conditions[i].value + ) { + matching = false; + } + }); + return matching; + }; + + const matchResult = (interaction: Interaction, set: Control) => { + let interactionResult = interaction.settings.result + let setResult = set.result + let matching = true + // if anything in the result for the interaction is different from the set make matching false + if(interactionResult?.type !== setResult?.type || + interactionResult?.mode !== setResult?.mode || + interactionResult?.value !== setResult?.value || + interactionResult?.dutyCycle !== setResult?.dutyCycle + ){ + matching = false + } + return matching + } + + const findDevice = (deviceID: number) => { + for (let device of devices) { + if (device.id() === deviceID) return device + } + } + + const findSinkComponent = (sinks: Component[], interactionSink: quack.ComponentID, sourceDevice: number) => { + for (let component of sinks) { + if (sameComponentID(component.location(), interactionSink) && sourceDevice === componentDevices.get(component.key())){ + return component + } + } + } + + useEffect(() => { + const newInteractions: Interaction[] = []; + const interactionSourceMap = new Map(); + const includedOps: quack.ComponentType[] = []; + const typeOps: Option[] = []; + const sinkOptions: Component[] = []; + const alerts: Alert[] = []; + const controls: Control[] = []; + + const run = async () => { + // Run all component API fetches in parallel + await Promise.all( + [...linkedComponents].map(async ([key, comp]) => { + const device = componentDevices.get(key); + + if (device) { + const resp = await interactionsAPI.listInteractionsByComponent( + device, + comp.location(), + undefined, + as + ); + + resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key)); + newInteractions.push(...resp); + } + + // Collect component types + if (!includedOps.includes(comp.type())) { + includedOps.push(comp.type()); + typeOps.push({ + label: getFriendlyName(comp.type()), + value: comp.type(), + }); + } + + // Collect controller components + if (extension(comp.type()).isController) { + sinkOptions.push(comp); + } + }) + ); + }; + + run().then(() => { + newInteractions.forEach(interaction => { + //alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications + if (interaction.isAlert()) { + // Find matching alert + let similarAlertIndex = alerts.findIndex(alert => + matchConditions(interaction, alert) + ); + + const compKey = interactionSourceMap.get(interaction.key()); + const component = compKey ? linkedComponents.get(compKey) : undefined; + + if (component) { + if (similarAlertIndex === -1) { + alerts.push({ + conditions: interaction.settings.conditions, + sourceComponents: [component], + }); + } else { + alerts[similarAlertIndex].sourceComponents.push(component); + } + } + + } + if (interaction.isControl()) { + if (!interaction.settings.sink) return //if there is no sink it cant be a control so move on to the next + const compKey = interactionSourceMap.get(interaction.key()); + if (!compKey) return //we have to have a valid component key + const component = linkedComponents.get(compKey); + const deviceID = componentDevices.get(compKey); + if (!deviceID) return //we have to know which device it is for + const controlDevice = findDevice(deviceID) + if (!controlDevice) return //failed to get the device from the array + const sinkComponent = findSinkComponent(sinkOptions, interaction.settings.sink, deviceID) + if (!sinkComponent) return //failed to get the correct sink component + + let similarControlIndex = controls.findIndex(control => + matchConditions(interaction, control) && + matchResult(interaction, control) && + sameComponentID(interaction.settings.sink, control.sinkComponent.location()) && //if the sink for the interaction is the same address as the existing control + deviceID === control.device.id() //if the device id for source component is the same as the device id in the control + ); + + if (component && interaction.settings.result) { + if (similarControlIndex === -1) { + controls.push({ + conditions: interaction.settings.conditions, + result: interaction.settings.result, + sourceComponents: [component], + sinkComponent: sinkComponent, + device: controlDevice + }); + } else { + controls[similarControlIndex].sourceComponents.push(component); + } + } + } + }); + setTypeOptions(typeOps); + setSinkOptions(sinkOptions); + setAlerts(alerts); + console.log(alerts) + setControls(controls); + console.log(controls) + }).catch(err => { + console.error("Interaction fetch error:", err) + }) + }, [linkedComponents, interactionsAPI, componentDevices, as]); + + + + return ( + + + Interactions + + + {/* */} + + + ) +} \ No newline at end of file diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 1fb51fb..4776ec7 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -69,6 +69,7 @@ import TaskViewer from "tasks/TaskViewer"; import ButtonGroup from "common/ButtonGroup"; import BinTransactions from "bin/BinTransactions"; import { CO2 } from "models/CO2"; +import ObjectInteractions from "objects/objectInteractions/ObjectInteractions"; interface TabPanelProps { children?: React.ReactNode; @@ -896,11 +897,16 @@ export default function Bin(props: Props) { {detail === "alerts" && ( - */} + )} @@ -1033,11 +1039,16 @@ export default function Bin(props: Props) { - */} + From ce7c90f3841f8e42c6223813b6e1444710d42b22 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 27 Nov 2025 17:08:14 -0600 Subject: [PATCH 13/20] getting the new object interaction dialog filtering the components properly regarding devices being passed in, finished the display part for alert and notification --- src/objects/objectInteractions/Alerts.tsx | 24 +++- src/objects/objectInteractions/Controls.tsx | 72 +++++++++--- .../NewObjectInteraction.tsx | 92 +++++++++++++-- .../objectInteractions/Notifications.tsx | 107 ++++++++++++++++++ .../objectInteractions/ObjectInteractions.tsx | 67 +++++++---- src/pages/Bin.tsx | 53 +-------- 6 files changed, 314 insertions(+), 101 deletions(-) diff --git a/src/objects/objectInteractions/Alerts.tsx b/src/objects/objectInteractions/Alerts.tsx index 0c1ab01..562216b 100644 --- a/src/objects/objectInteractions/Alerts.tsx +++ b/src/objects/objectInteractions/Alerts.tsx @@ -1,10 +1,11 @@ -import { ExpandMore } from "@mui/icons-material"; -import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, List, ListItem, ListSubheader, Typography } from "@mui/material"; +import { Add, ExpandMore } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material"; import { Component } from "models"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { pond, quack } from "protobuf-ts/pond"; import React, { useState } from "react"; import NewObjectInteraction from "./NewObjectInteraction"; +import { Option } from "common/SearchSelect"; export interface Alert { conditions: pond.InteractionCondition[]; @@ -13,11 +14,12 @@ export interface Alert { interface Props { alerts: Alert[] + addNew?: () => void } export default function Alerts(props: Props){ - const {alerts} = props - const [openNewAlert, setOpenNewAlert] = useState(false) + const {alerts, addNew} = props + // const [openNewAlert, setOpenNewAlert] = useState(false) const readableCondition = (condition: pond.InteractionCondition) => { let describer = describeMeasurement(condition.measurementType); @@ -62,9 +64,19 @@ export default function Alerts(props: Props){ return ( - {}}/> + {/* { + setOpenNewAlert(false) + if(refresh && refreshCallback) refreshCallback() + }} typeOptions={typeOptions} linkedComponents={linkedComponents}/> */} - Alerts + + + Alerts + + {addNew && + {addNew()}} color="primary"> + } + {alerts.map((alert, i) => ( {alertAccordion(alert)} ))} diff --git a/src/objects/objectInteractions/Controls.tsx b/src/objects/objectInteractions/Controls.tsx index 096c9ca..9498fed 100644 --- a/src/objects/objectInteractions/Controls.tsx +++ b/src/objects/objectInteractions/Controls.tsx @@ -1,4 +1,5 @@ -import { Accordion, AccordionDetails, AccordionSummary, List, ListItem, ListSubheader } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material"; import { Component, Device } from "models"; import { pond } from "protobuf-ts/pond"; import React, { useEffect, useState } from "react"; @@ -12,17 +13,20 @@ export interface Control { } interface Props { + devices: Device[] controls: Control[] + addNew?: (deviceID: number) => void } export default function Controls(props: Props){ - const { controls } = props + const { devices, controls, addNew } = props const [deviceControls, setDeviceControls] = useState>(new Map()) - const [deviceNameMap, setDeviceNameMap] = useState>(new Map()) + const [openDeviceMenu, setOpenDeviceMenu] = useState(false) + const [anchor, setAnchor] = useState(null) + useEffect(()=>{ let map: Map = new Map() - let nameMap: Map = new Map() controls.forEach(control => { let mappedControls = map.get(control.device.id()) if(mappedControls){ @@ -30,30 +34,72 @@ export default function Controls(props: Props){ }else{ map.set(control.device.id(), [control]) } - if(!nameMap.get(control.device.id())){ - nameMap.set(control.device.id(), control.device.name()) - } }) setDeviceControls(map) - setDeviceNameMap(nameMap) },[controls]) const controlAccordion = (control: Control) => { return ( - {control.device.name()} - + Show what the control does + show the components that are doing it ) } + const deviceMenu = () => { + return ( + { + setAnchor(null) + setOpenDeviceMenu(false) + }}> + {devices.map(dev => ( + { + setAnchor(null) + setOpenDeviceMenu(false) + if (addNew) addNew(dev.id()) + + }}> + {dev.name()} + + ))} + + ) + } return ( + {deviceMenu()} - Controls - {controls.map((control, i) => ( - {controlAccordion(control)} + + + Alerts + + { + setOpenDeviceMenu(true) + setAnchor(e.currentTarget) + }} + color="primary"> + + + + {devices.map(device => ( + + {device.name()} + + {deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => ( + {controlAccordion(control)} + )) + : + No Control Interactions For Device + } + ))} diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx index 31170e3..b868fb7 100644 --- a/src/objects/objectInteractions/NewObjectInteraction.tsx +++ b/src/objects/objectInteractions/NewObjectInteraction.tsx @@ -1,6 +1,8 @@ -import { Button, DialogActions, DialogContent, DialogTitle, MenuItem, Select } from "@mui/material"; +import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; -import { getMeasurements } from "pbHelpers/ComponentType"; +import { Option } from "common/SearchSelect"; +import { Component } from "models"; +import { extension, getMeasurements } from "pbHelpers/ComponentType"; import { Measurement, Operator } from "pbHelpers/Enums"; import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; @@ -9,19 +11,58 @@ import { useEffect, useState } from "react"; interface Props { open: boolean onClose: (refreshCallback: boolean) => void + linkedComponents: Map + componentDevices: Map //if we pass in a device id we could filter the components to only be for that device, this could be used for the control part device?: number } export default function NewObjectInteraction(props: Props){ - const { open, onClose } = props + const { open, onClose, device, linkedComponents, componentDevices } = props const [newAlertComponentType, setNewAlertComponentType] = useState( quack.ComponentType.COMPONENT_TYPE_INVALID ); const [conditions, setConditions] = useState([]); const [selectedAlertComponents, setSelectedAlertComponents] = useState([]); + const [validOptions, setValidOptions] = useState([]) + const [validComponents, setValidComponents] = useState([]) - useEffect(() => {},[]) + useEffect(() => { + //if the device is passed in need to filter out possible components + let validCompList: Component[] = [] + let validOptions: Option[] = [] + + linkedComponents.forEach((comp, key) => { + if(componentDevices.get(key) === device || !device){ + validCompList.push(comp) + } + }) + + let included: quack.ComponentType[] = [] + validCompList.forEach(comp => { + if(included.includes(comp.type())) return + let ext = extension(comp.type(), comp.subType()) + if(device){ + if(!ext.isController){ + included.push(comp.type()) + validOptions.push({ + label: ext.friendlyName, + value: comp.type() + }) + } + }else{ + included.push(comp.type()) + validOptions.push({ + label: ext.friendlyName, + value: comp.type() + }) + } + + }) + + setValidOptions(validOptions) + setValidComponents(validCompList) + },[device, linkedComponents, componentDevices]) const close = (refresh: boolean) => { @@ -41,6 +82,39 @@ export default function NewObjectInteraction(props: Props){ return [condition]; }; + //display the components that match the type that was selected for the new alert + const listMatchingComponents = () => { + let matching: Component[] = []; + validComponents.forEach(comp => { + if (comp.type() === newAlertComponentType) { + matching.push(comp); + } + }); + return ( + + {matching.map(comp => ( + + + { + let selected = selectedAlertComponents; + if (checked) { + selected.push(comp.key()); + } else { + selected.splice(selected.indexOf(comp.key()), 1); + } + setSelectedAlertComponents([...selected]); + }} + /> + + {comp.name()} + + ))} + + ); + }; + return ( Select Component Type - {typeOptions.map(option => ( + {validOptions.map(option => ( {option.label} @@ -73,21 +147,21 @@ export default function NewObjectInteraction(props: Props){ {/* list of checkboxes for the components that match that type */} {listMatchingComponents()} {/* have the conditions set here */} - {conditionInput()} + {/* {conditionInput()} */} {/* have the schedule set here */} - {scheduler()} + {/* {scheduler()} */} - + */} ) diff --git a/src/objects/objectInteractions/Notifications.tsx b/src/objects/objectInteractions/Notifications.tsx index e69de29..aa2e410 100644 --- a/src/objects/objectInteractions/Notifications.tsx +++ b/src/objects/objectInteractions/Notifications.tsx @@ -0,0 +1,107 @@ +import { Button, List, ListItem, ListSubheader, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import moment from "moment"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState, useNotificationAPI } from "providers"; +import React, { useEffect, useRef, useState } from "react"; +import { getThemeType } from "theme/themeType"; + +interface Props { + objectKey: string + objectType: pond.ObjectType +} + +const useStyles = makeStyles(() => { + return ({ + dark: { + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", + padding: 5 + }, + light: { + backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)", + padding: 5 + }, + }) +}) + +export default function Notifications(props: Props){ + const {objectKey, objectType} = props + const classes = useStyles(); + const [recentNotifications, setRecentNotifications] = useState([]); + const [notificationsLoading, setNotificationsLoading] = useState(false) + const loadingRef = useRef(false) + const [{as}] = useGlobalState(); + const notificationAPI = useNotificationAPI(); + const [totalNotifications, setTotalNotifications] = useState(0) + + + //get the notifications for the components on an object + useEffect(() => { + if (loadingRef.current) return + loadingRef.current = true + notificationAPI + .listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as) + .then(resp => { + setRecentNotifications(resp.data.notifications); + setTotalNotifications(resp.data.total); + }) + .catch(_err => {}) + .finally(() => { + loadingRef.current = true; + }); + }, [objectKey, objectType, notificationAPI]); + + const loadMoreNotifications = () => { + let current = recentNotifications; + setNotificationsLoading(true); + notificationAPI + .listObjectNotifications( + objectKey, + objectType, + 10, + current.length, + undefined, + undefined, + undefined, + as + ) + .then(resp => { + if (resp.data.notifications.length > 0) { + let c = current.concat(resp.data.notifications); + setRecentNotifications([...c]); + } + }) + .catch(_err => {}) + .finally(() => { + setNotificationsLoading(false); + }); + }; + + return ( + + Notifications + {recentNotifications && ( + + {recentNotifications.map((notification, i) => ( + + + {moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")} + {" "} + :{" "} + + {notification.settings?.title + " - " + notification.settings?.subtitle} + + + ))} + {recentNotifications.length < totalNotifications && ( + + + + )} + + )} + + ); +} \ No newline at end of file diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx index aa9f269..51e3230 100644 --- a/src/objects/objectInteractions/ObjectInteractions.tsx +++ b/src/objects/objectInteractions/ObjectInteractions.tsx @@ -9,23 +9,32 @@ import { useInteractionsAPI } from "hooks"; import { useGlobalState } from "providers"; import { extension, getFriendlyName } from "pbHelpers/ComponentType"; import { sameComponentID } from "pbHelpers/Component"; +import Notifications from "./Notifications"; +import { pond } from "protobuf-ts/pond"; +import React from "react"; +import NewObjectInteraction from "./NewObjectInteraction"; interface Props { linkedComponents: Map; //the component key to the component object componentDevices: Map; + objectKey: string + objectType: pond.ObjectType devices: Device[]; + refreshCallback?: () => {} } export default function ObjectInteractions(props: Props) { - const { linkedComponents, componentDevices, devices } = props + const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback } = props const [{as}] = useGlobalState() //list of alerts, each alert contains a list of interactions with matching conditions const [alerts, setAlerts] = useState([]); //list of interactions relating to a controller const [controls, setControls] = useState([]); const interactionsAPI = useInteractionsAPI(); - const [sinkOptions, setSinkOptions] = useState([]) - const [typeOptions, setTypeOptions] = useState([]); + const [openNewInteraction, setOpenNewInteraction] = useState(false) + const [allSinks, setAllSinks] = useState([]); + // const [typeOptions, setTypeOptions] = useState([]); + const [selectedDevice, setSelectedDevice] = useState(undefined) const matchConditions = (interaction: Interaction, set: Alert | Control) => { @@ -79,8 +88,6 @@ export default function ObjectInteractions(props: Props) { useEffect(() => { const newInteractions: Interaction[] = []; const interactionSourceMap = new Map(); - const includedOps: quack.ComponentType[] = []; - const typeOps: Option[] = []; const sinkOptions: Component[] = []; const alerts: Alert[] = []; const controls: Control[] = []; @@ -103,15 +110,6 @@ export default function ObjectInteractions(props: Props) { newInteractions.push(...resp); } - // Collect component types - if (!includedOps.includes(comp.type())) { - includedOps.push(comp.type()); - typeOps.push({ - label: getFriendlyName(comp.type()), - value: comp.type(), - }); - } - // Collect controller components if (extension(comp.type()).isController) { sinkOptions.push(comp); @@ -178,8 +176,7 @@ export default function ObjectInteractions(props: Props) { } } }); - setTypeOptions(typeOps); - setSinkOptions(sinkOptions); + setAllSinks(sinkOptions); setAlerts(alerts); console.log(alerts) setControls(controls); @@ -192,13 +189,35 @@ export default function ObjectInteractions(props: Props) { return ( - - - Interactions - - - {/* */} - - + + { + setOpenNewInteraction(false) + if(refresh && refreshCallback){ + refreshCallback() + } + }} + linkedComponents={linkedComponents} + componentDevices={componentDevices} + device={selectedDevice}/> + + + Interactions + { + setSelectedDevice(deviceID) + setOpenNewInteraction(true) + }}/> + { + setSelectedDevice(undefined) + setOpenNewInteraction(true) + }}/> + + + + ) } \ No newline at end of file diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 4776ec7..37c2d62 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -857,56 +857,15 @@ export default function Bin(props: Props) { } ]} /> - {/* - setDetail("inventory")}> - Inventory - - setDetail("sensors")} - value={"sensors"} - aria-label="Bin Sensor Graphs"> - Sensors - - setDetail("analytics")} - value={"analytics"} - aria-label="Bin Analysis Graphs"> - Analysis - - setDetail("alerts")} - value={"alerts"} - aria-label="Device Alerts"> - Alerts - - setDetail("presets")} - value={"presets"} - aria-label="Bin Mode Presets"> - Presets - - */} {detail === "alerts" && ( - {/* */} )} @@ -1039,16 +998,12 @@ export default function Bin(props: Props) { - {/* */} From bb92077d0b522cef92fbf10ea61c661e25ddd71a Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 28 Nov 2025 16:36:59 -0600 Subject: [PATCH 14/20] finished the form for creating an interaction/alert for a device through an object --- src/objects/ObjectAlerts.tsx | 973 ------------------ src/objects/objectInteractions/Alerts.tsx | 9 +- src/objects/objectInteractions/Controls.tsx | 8 +- .../NewObjectInteraction.tsx | 908 +++++++++++++++- .../objectInteractions/ObjectInteractions.tsx | 19 +- src/pages/Bin.tsx | 4 +- 6 files changed, 916 insertions(+), 1005 deletions(-) delete mode 100644 src/objects/ObjectAlerts.tsx diff --git a/src/objects/ObjectAlerts.tsx b/src/objects/ObjectAlerts.tsx deleted file mode 100644 index ef443dc..0000000 --- a/src/objects/ObjectAlerts.tsx +++ /dev/null @@ -1,973 +0,0 @@ -import { - Accordion, - AccordionDetails, - AccordionSummary, - Box, - Button, - Card, - Checkbox, - DialogActions, - DialogContent, - DialogTitle, - FormControl, - FormControlLabel, - FormHelperText, - FormLabel, - Grid2 as Grid, - IconButton, - InputAdornment, - List, - ListItem, - ListItemIcon, - ListItemText, - ListSubheader, - MenuItem, - Select, - TextField, - Theme, - Tooltip, - Typography -} from "@mui/material"; -import { ExpandMore, RemoveCircle as RemoveIcon, AddCircle as AddIcon } from "@mui/icons-material"; -import ResponsiveDialog from "common/ResponsiveDialog"; -import { Option } from "common/SearchSelect"; -import classNames from "classnames"; -import { Component, Interaction } from "models"; -import moment from "moment"; -import { getFriendlyName, getMeasurements } from "pbHelpers/ComponentType"; -import { Measurement, Operator } from "pbHelpers/Enums"; -import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; -import { pond } from "protobuf-ts/pond"; -import { quack } from "protobuf-ts/quack"; -import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers"; -import { useNotificationAPI } from "providers/pond/notificationAPI"; -import React, { useEffect, useState } from "react"; -import { capitalize, cloneDeep } from "lodash"; -import { timeOfDayDescriptor } from "pbHelpers/Interaction"; -import { TimePicker } from "@mui/x-date-pickers"; -import { getThemeType } from "theme/themeType"; -import { green, red } from "@mui/material/colors"; -import { makeStyles } from "@mui/styles"; - - - -interface Props { - linkedComponents: Map; - componentDevices: Map; - objectType: pond.ObjectType; - objectKey: string; -} - -interface Alert { - conditions: pond.InteractionCondition[]; - components: Component[]; -} - -const useStyles = makeStyles((theme: Theme) => { - return ({ - borderedContainer: { - padding: theme.spacing(1), - marginBottom: theme.spacing(2), - border: "1px solid rgba(255, 255, 255, 0.12)", - borderRadius: "4px" - }, - greenButton: { - color: green["600"] - }, - redButton: { - color: red["600"] - }, - noPadding: { - padding: 0 - }, - timeRange: { - fontSize: "0.875em", - marginTop: "20px" - }, - dark: { - backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", - padding: 5 - }, - light: { - backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)", - padding: 5 - } - }); -}); - -export default function ObjectAlerts(props: Props) { - const { linkedComponents, componentDevices, objectKey, objectType } = props; - const classes = useStyles(); - const { openSnack } = useSnackbar(); - const [{ as }] = useGlobalState(); - const interactionsAPI = useInteractionsAPI(); - const [interactions, setInteractions] = useState([]); - const [typeOptions, setTypeOptions] = useState([]); - const [alerts, setAlerts] = useState([]); - //the interaction key to the component it is set on - const [interactionComponent, setInteractionComponent] = useState>(new Map()); - - //state variables for notifications - const notificationAPI = useNotificationAPI(); - const [recentNotifications, setRecentNotifications] = useState([]); - const [notificationsLoading, setNotificationsLoading] = useState(false); - const [totalNotifications, setTotalNotifications] = useState(0); - - //stat variables for adding new alerts - const [newAlertOpen, setNewAlertOpen] = useState(false); - const [selectedAlertComponents, setSelectedAlertComponents] = useState([]); - const [newAlertComponentType, setNewAlertComponentType] = useState( - quack.ComponentType.COMPONENT_TYPE_INVALID - ); - const [conditions, setConditions] = useState([]); - const [nodeOptions, setNodeOptions] = useState([]); - const [subtypeDropdown, setSubtypeDropdown] = useState(0); - //condition value strings - so that decimals are easier to type into the field - const [valStrings, setValStrings] = useState(["0", "0"]); - //the nodes for the subnode interactions - const [nodeOne, setNodeOne] = useState(0); - const [nodeTwo, setNodeTwo] = useState(0); - - //schedule variables - const [schedule, setSchedule] = useState( - pond.InteractionSchedule.create({ - weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] - }) - ); - - useEffect(() => { - //loop through the linked components to get each of their interactions - //TODO-CS: consider making an api function to get the interactions for multiple components, possibly based off objects, in one call, - //to get all interactions for bin components this would solve the issue of making multiple calls as well as not having to use async await - let newInteractions: Interaction[] = []; - let icMap: Map = new Map(); - let includedOps: quack.ComponentType[] = []; - let typeOps: Option[] = []; - let index = 0; - //use async to make sure to wait for the response from the api call and have the interactions set before going to get the next one - //this prevents seperate responses from assigning to the array without knowing about interactions from those seperate responses - //for example: without async two responses come back at the same time, one has two interactions and another has one but, neither have put their interactions - //into the array yet so they both reference an empty array, the first response puts its two into an empty array and sets it but then the next one also puts - //its response into an empty array as well and sets it causing the first two to be lost - linkedComponents.forEach(async (comp, key) => { - index++; - //get the interactions for the component - let device = componentDevices.get(key); - if (device !== undefined) { - await interactionsAPI.listInteractionsByComponent(device, comp.location(), undefined, as).then(resp => { - if (resp.length > 0) { - resp.forEach(interaction => { - icMap.set(interaction.key(), key); - }); - newInteractions = newInteractions.concat(resp); - } - }); - } - //set the state variables if it is the last one in the list - if (index === linkedComponents.size) { - setInteractionComponent(icMap); - setInteractions([...newInteractions]); - } - //use the component type to the type options if it is not already present - if (!includedOps.includes(comp.type())) { - includedOps.push(comp.type()); - typeOps.push({ - label: getFriendlyName(comp.type()), - value: comp.type() - }); - } - }); - setTypeOptions(typeOps); - }, [linkedComponents, interactionsAPI, componentDevices]); - - const matchConditions = (interaction: Interaction, alert: Alert) => { - //if the number of conditions are not the same they are different - if (interaction.settings.conditions.length !== alert.conditions.length) { - return false; - } - //continure with the comparison - let matching = true; - interaction.settings.conditions.forEach((condition, i) => { - if ( - condition.comparison !== alert.conditions[i].comparison || - condition.measurementType !== alert.conditions[i].measurementType || - condition.value !== alert.conditions[i].value - ) { - matching = false; - } - }); - return matching; - }; - - //build the alerts to display - useEffect(() => { - //loop through the interactions - let currentAlerts: Alert[] = []; - interactions.forEach(interaction => { - //if the interaction sends notifications - if (interaction.settings.notifications && interaction.settings.notifications.notify) { - //determine if the interaction already has an alert in the alerts array - let similarAlert: number = -1; - currentAlerts.forEach((alert, i) => { - if (matchConditions(interaction, alert)) { - similarAlert = i; - } - }); - - //if no current alert matched the conditions add a new alert to the array - let compKey = interactionComponent.get(interaction.key()); - if (compKey) { - let component = linkedComponents.get(compKey); - if (component) { - if (similarAlert === -1) { - let newAlert: Alert = { - conditions: interaction.settings.conditions, - components: [component] - }; - currentAlerts.push(newAlert); - } else { - currentAlerts[similarAlert].components.push(component); - } - } - } - } - }); - setAlerts(currentAlerts); - }, [interactions, interactionComponent, linkedComponents]); - - //get the notifications for the components on an object - useEffect(() => { - if (notificationsLoading) return; - setNotificationsLoading(true); - notificationAPI - .listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as) - .then(resp => { - setRecentNotifications(resp.data.notifications); - setTotalNotifications(resp.data.total); - }) - .catch(_err => {}) - .finally(() => { - setNotificationsLoading(false); - }); - }, [objectKey, objectType, notificationAPI]); //eslint-disable-line react-hooks/exhaustive-deps - - //use the selected components to determine the lowest amount of nodes for the options - useEffect(() => { - setNodeOne(0); - setNodeTwo(0); - setSubtypeDropdown(0); - let nodeCount = 12; //start with the maximum number of nodes for a cable - linkedComponents.forEach(comp => { - if (selectedAlertComponents.includes(comp.key())) { - if (comp.lastMeasurement[0] && comp.lastMeasurement[0].values[0]) { - let nodes = comp.lastMeasurement[0].values[0].values.length; - nodeCount = nodes < nodeCount ? nodes : nodeCount; - } - } - }); - let options = []; - for (let i = 0; i <= nodeCount; i++) { - options.push( - - {i === 0 ? "None" : "Node " + i} - - ); - } - setNodeOptions(options); - }, [linkedComponents, selectedAlertComponents]); - - const loadMoreNotifications = () => { - console.log("load more"); - let current = recentNotifications; - setNotificationsLoading(true); - notificationAPI - .listObjectNotifications( - objectKey, - objectType, - 10, - current.length, - undefined, - undefined, - undefined, - as - ) - .then(resp => { - if (resp.data.notifications.length > 0) { - let c = current.concat(resp.data.notifications); - setRecentNotifications([...c]); - } - }) - .catch(_err => {}) - .finally(() => { - setNotificationsLoading(false); - }); - }; - - const readableCondition = (condition: pond.InteractionCondition) => { - let describer = describeMeasurement(condition.measurementType); - let type = describer.label(); - let comparison = - condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO - ? "Exactly" - : condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN - ? "Above" - : "Below"; - let value = describer.toDisplay(condition.value); - return ( - - {type + " " + comparison + " " + value + describer.GetUnit()} - - ); - }; - - const alertAccordion = (alert: Alert) => { - return ( - - }> - - {alert.conditions.map((condition, i) => ( - - {readableCondition(condition)} - - ))} - - - - - Reporting Components - {alert.components.map((comp, i) => ( - {comp.name()} - ))} - - - - ); - }; - - const addInteractionToComponents = () => { - //array of device and component ids ie, [4:9-5-12, 4:9-5-13, 3:9-5-12] - let compIds: string[] = []; - selectedAlertComponents.forEach(comp => { - let devID = componentDevices.get(comp); - let component = linkedComponents.get(comp); - if (devID && component) { - compIds.push(devID + ":" + component.locationString()); - } - }); - let newAlert = pond.InteractionSettings.create(); - newAlert.conditions = conditions; - newAlert.instance = 1; - newAlert.schedule = schedule; - newAlert.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo); - newAlert.result = pond.InteractionResult.create({ - type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT - }); - newAlert.notifications = pond.InteractionNotifications.create({ - notify: true - }); - interactionsAPI - .addInteractionToComponents(compIds, newAlert, as) - .then(_resp => { - openSnack("interaction added to selected components"); - }) - .catch(_err => { - openSnack("there was a problem adding the interaction to the selected components"); - }); - setNewAlertOpen(false); - }; - - //display the components that match the type that was selected for the new alert - const listMatchingComponents = () => { - let matching: Component[] = []; - linkedComponents.forEach(comp => { - if (comp.type() === newAlertComponentType) { - matching.push(comp); - } - }); - return ( - - {matching.map(comp => ( - - - { - let selected = selectedAlertComponents; - if (checked) { - selected.push(comp.key()); - } else { - selected.splice(selected.indexOf(comp.key()), 1); - } - setSelectedAlertComponents([...selected]); - }} - /> - - {comp.name()} - - ))} - - ); - }; - - const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => { - return getMeasurements(type).map(m => m.measurementType); - }; - - const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => { - let measurementType = availableMeasurementTypes(type)[0]; - let condition = pond.InteractionCondition.create({ - measurementType: measurementType, - comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less, - value: 0 - }); - return [condition]; - }; - - const measurementTypeMenuItems = () => { - return availableMeasurementTypes(newAlertComponentType).map(measurementType => ( - - {describeMeasurement(measurementType).label()} - - )); - }; - - const changeCondition = (newCondition: pond.InteractionCondition, index: number) => { - let c = conditions; - c[index] = newCondition; - setConditions([...c]); - }; - - const conditionGroup = (condition: pond.InteractionCondition, index: number) => { - let measurementType = condition.measurementType; - let describer = describeMeasurement(measurementType, newAlertComponentType); - let isBoolean = condition.measurementType === Measurement.boolean; - return ( - - - {index > 0 && ( - - AND - - )} - - { - condition.measurementType = +event.target.value; - changeCondition(condition, index); - }} - autoFocus={false} - margin="dense" - variant="standard" - fullWidth - InputLabelProps={{ shrink: true }}> - {measurementTypeMenuItems()} - - - - { - condition.comparison = +event.target.value; - changeCondition(condition, index); - }} - autoFocus={false} - margin="dense" - variant="standard" - fullWidth - InputLabelProps={{ shrink: true }}> - {!isBoolean && [ - - {"is below"} - , - - {"is above"} - - ]} - - {"is exactly"} - - - - - { - let strings = valStrings; - strings[index] = event.target.value; - setValStrings([...strings]); - if (!isNaN(+valStrings[index])) { - condition.value = describer.toStored(+event.target.value); - changeCondition(condition, index); - } - }} - autoFocus={false} - margin="dense" - variant="standard" - fullWidth - InputProps={{ - endAdornment: {describer.unit()} - }} - InputLabelProps={{ shrink: true }}> - {isBoolean && [ - - {describer.enumerations()[0]} - , - - {describer.enumerations()[1]} - - ]} - - - - {index === 0 ? ( - - 1} - aria-label="Add another condition" - onClick={() => { - let c = cloneDeep(conditions); - let newConditions = c.concat(initialConditions(newAlertComponentType)); - setConditions(newConditions); - }} - className={classNames(classes.greenButton, classes.noPadding)}> - - - - ) : ( - - { - let c = conditions; - c.splice(index, 1); - setConditions([...c]); - }} - className={classNames(classes.redButton, classes.noPadding)}> - - - - )} - - - {nodeOptions.length > 2 && ( - - - { - setSubtypeDropdown(+event.target.value); - }} - margin="dense" - variant="standard" - fullWidth - InputLabelProps={{ shrink: true }}> - - Any - - - Average - - - Single Node - - - Node Diff - - - Up To - - - - - {(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && ( - { - setNodeOne(+event.target.value); - }} - margin="dense" - variant="standard" - fullWidth - InputLabelProps={{ shrink: true }}> - {nodeOptions} - - )} - - - {subtypeDropdown === 3 && ( - { - setNodeTwo(+event.target.value); - }} - margin="dense" - variant="standard" - fullWidth - InputLabelProps={{ shrink: true }}> - {nodeOptions} - - )} - - - )} - - ); - }; - - const conditionInput = () => { - // const conditionGroups: JSX.Element[] = []; - // for (let i = 0; i < conditions.length; i++) { - // conditionGroups[i] = conditionGroup(i); - // } - return ( - - Conditions - {selectedAlertComponents.length > 0 ? ( - - {conditions.map((condition, i) => conditionGroup(condition, i))} - - ) : ( - You must select a source before adding conditions - )} - - ); - }; - - const setScheduleTime = (id: string, event: any) => { - let updatedSchedule = cloneDeep(schedule); - if (id === "shortcut") { - let value = event.target.value; - if (value === "allDay") { - updatedSchedule.timeOfDayStart = "00:00"; - updatedSchedule.timeOfDayEnd = "24:00"; - } else if (value === "before") { - updatedSchedule.timeOfDayStart = "00:00"; - if (!updatedSchedule.timeOfDayEnd || updatedSchedule.timeOfDayEnd === "24:00") { - updatedSchedule.timeOfDayEnd = "23:59"; - } - } else if (value === "after") { - updatedSchedule.timeOfDayEnd = "24:00"; - if (updatedSchedule.timeOfDayStart === "00:00") { - updatedSchedule.timeOfDayStart = "00:01"; - } - } else { - if (updatedSchedule.timeOfDayStart === "00:00") { - updatedSchedule.timeOfDayStart = "00:01"; - } - if (updatedSchedule.timeOfDayEnd === "24:00") { - updatedSchedule.timeOfDayEnd = "23:59"; - } - } - } else { - let timeOfDay = - event - .hour() - .toString() - .padStart(2, "0") + - ":" + - event - .minute() - .toString() - .padStart(2, "0"); - if (id === "start") { - updatedSchedule.timeOfDayStart = timeOfDay; - } else if (id === "end") { - if (timeOfDay === "00:00") { - timeOfDay = "24:00"; - } - updatedSchedule.timeOfDayEnd = timeOfDay; - } - } - updatedSchedule.timezone = moment.tz.guess(); - setSchedule(updatedSchedule); - }; - - const toggleDaySelected = (day: string, event: any) => { - let updatedSchedule = cloneDeep(schedule); - if (event.target.checked) { - if (!updatedSchedule.weekdays.includes(day)) { - updatedSchedule.weekdays.push(day); - } - } else { - updatedSchedule.weekdays.splice(updatedSchedule.weekdays.indexOf(day), 1); - } - setSchedule(updatedSchedule); - }; - - const daySelector = (day: string, size: any) => { - //const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create()); - return ( - - toggleDaySelected(day, event)} - value={day} - color="secondary" - /> - } - label={capitalize(day.substr(0, 2))} - labelPlacement="bottom" - /> - - ); - }; - - const scheduler = () => { - //let schedule = pond.InteractionSchedule.create(); - let timezone = moment.tz.guess(); - let todStart = "00:00".split(":"); - let todEnd = "23:59".split(":"); - let start = moment - .tz(timezone) - .startOf("day") - .add(todStart[0], "hours") - .add(todStart[1], "minutes") - .local(); - let end = moment - .tz(timezone) - .startOf("day") - .add(todEnd[0], "hours") - .add(todEnd[1], "minutes") - .local(); - let descriptor = timeOfDayDescriptor(schedule); - let showStart = descriptor === "after" || descriptor === "from"; - let showEnd = descriptor === "before" || descriptor === "from"; - let showBoth = showStart && showEnd; - return ( - - Schedule - - {daySelector("sunday", 3)} - {daySelector("monday", 3)} - {daySelector("tuesday", 3)} - {daySelector("wednesday", 3)} - {daySelector("thursday", 4)} - {daySelector("friday", 4)} - {daySelector("saturday", 4)} - - - - setScheduleTime("shortcut", event)} - fullWidth - autoFocus={false} - margin="normal" - variant="standard" - InputLabelProps={{ shrink: true }}> - - All Day - - - Before - - - After - - - From - - - - {showStart && ( - - } - value={start} - onChange={(event: any) => setScheduleTime("start", event)} - /> - - )} - {showBoth && ( - - - to - - - )} - {showEnd && ( - - } - value={end} - onChange={(event: any) => setScheduleTime("end", event)} - /> - - )} - - - ); - }; - - const newAlertDialog = () => { - return ( - { - setNewAlertOpen(false); - }}> - Set New Alert - - {/* Dropdown to select the component type to add the interaction to */} - - {/* list of checkboxes for the components that match that type */} - {listMatchingComponents()} - {/* have the conditions set here */} - {conditionInput()} - {/* have the schedule set here */} - {scheduler()} - - - - - - - ); - }; - - const alertDisplay = () => { - return ( - - Alerts - {alerts.map((alert, i) => ( - {alertAccordion(alert)} - ))} - - ); - }; - - const notificationList = () => { - return ( - - Notifications - {recentNotifications && ( - - {recentNotifications.map((notification, i) => ( - - - {moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")} - {" "} - :{" "} - - {notification.settings?.title + " - " + notification.settings?.subtitle} - - - ))} - {recentNotifications.length < totalNotifications && ( - - - - )} - - )} - - ); - }; - - return ( - - - Alerts & Notifications - - - {alertDisplay()} - {notificationList()} - {newAlertDialog()} - - ); -} diff --git a/src/objects/objectInteractions/Alerts.tsx b/src/objects/objectInteractions/Alerts.tsx index 562216b..0875a22 100644 --- a/src/objects/objectInteractions/Alerts.tsx +++ b/src/objects/objectInteractions/Alerts.tsx @@ -14,11 +14,12 @@ export interface Alert { interface Props { alerts: Alert[] + permissions: pond.Permission[] addNew?: () => void } export default function Alerts(props: Props){ - const {alerts, addNew} = props + const {alerts, addNew, permissions} = props // const [openNewAlert, setOpenNewAlert] = useState(false) const readableCondition = (condition: pond.InteractionCondition) => { @@ -64,17 +65,13 @@ export default function Alerts(props: Props){ return ( - {/* { - setOpenNewAlert(false) - if(refresh && refreshCallback) refreshCallback() - }} typeOptions={typeOptions} linkedComponents={linkedComponents}/> */} Alerts {addNew && - {addNew()}} color="primary"> + {addNew()}} color="primary"> } {alerts.map((alert, i) => ( diff --git a/src/objects/objectInteractions/Controls.tsx b/src/objects/objectInteractions/Controls.tsx index 9498fed..1dc04ff 100644 --- a/src/objects/objectInteractions/Controls.tsx +++ b/src/objects/objectInteractions/Controls.tsx @@ -15,11 +15,12 @@ export interface Control { interface Props { devices: Device[] controls: Control[] - addNew?: (deviceID: number) => void + permissions: pond.Permission[] + addNew?: (device: Device) => void } export default function Controls(props: Props){ - const { devices, controls, addNew } = props + const { devices, controls, addNew, permissions } = props const [deviceControls, setDeviceControls] = useState>(new Map()) const [openDeviceMenu, setOpenDeviceMenu] = useState(false) const [anchor, setAnchor] = useState(null) @@ -62,7 +63,7 @@ export default function Controls(props: Props){ onClick={() => { setAnchor(null) setOpenDeviceMenu(false) - if (addNew) addNew(dev.id()) + if (addNew) addNew(dev) }}> {dev.name()} @@ -81,6 +82,7 @@ export default function Controls(props: Props){ Alerts { setOpenDeviceMenu(true) setAnchor(e.currentTarget) diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx index b868fb7..889e061 100644 --- a/src/objects/objectInteractions/NewObjectInteraction.tsx +++ b/src/objects/objectInteractions/NewObjectInteraction.tsx @@ -1,12 +1,26 @@ -import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select } from "@mui/material"; +import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormHelperText, FormLabel, Grid2, IconButton, InputAdornment, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select, Switch, TextField, Theme, Tooltip, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import classNames from "classnames"; import ResponsiveDialog from "common/ResponsiveDialog"; import { Option } from "common/SearchSelect"; -import { Component } from "models"; +import { capitalize, cloneDeep } from "lodash"; +import { Component, Device, Interaction } from "models"; import { extension, getMeasurements } from "pbHelpers/ComponentType"; -import { Measurement, Operator } from "pbHelpers/Enums"; +import { ComponentType, Measurement, Operator } from "pbHelpers/Enums"; +import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber"; import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; +import React from "react"; import { useEffect, useState } from "react"; +import AddIcon from "@mui/icons-material/AddCircle"; +import RemoveIcon from "@mui/icons-material/RemoveCircle"; +import { green, red } from "@mui/material/colors"; +import { or } from "utils"; +import moment from "moment"; +import { timeOfDayDescriptor } from "pbHelpers/Interaction"; +import { TimePicker } from "@mui/x-date-pickers"; +import { useInteractionsAPI, useSnackbar } from "hooks"; +import { useGlobalState } from "providers"; interface Props { open: boolean @@ -14,11 +28,45 @@ interface Props { linkedComponents: Map componentDevices: Map //if we pass in a device id we could filter the components to only be for that device, this could be used for the control part - device?: number + device?: Device } +const useStyles = makeStyles((theme: Theme) => { + return ( + { + borderedContainer: { + padding: theme.spacing(1), + marginBottom: theme.spacing(2), + border: "1px solid rgba(255, 255, 255, 0.12)", + borderRadius: "4px" + }, + greenButton: { + color: green["600"] + }, + redButton: { + color: red["600"] + }, + noPadding: { + padding: 0 + }, + forPrompt: { + fontSize: "0.875em", + marginTop: "10px" + }, + timeRange: { + fontSize: "0.875em", + marginTop: "20px" + }, + } + ) +}) + export default function NewObjectInteraction(props: Props){ const { open, onClose, device, linkedComponents, componentDevices } = props + const classes = useStyles() + const [{ as }] = useGlobalState(); + const {openSnack} = useSnackbar(); + const interactionsAPI = useInteractionsAPI(); const [newAlertComponentType, setNewAlertComponentType] = useState( quack.ComponentType.COMPONENT_TYPE_INVALID ); @@ -26,14 +74,49 @@ export default function NewObjectInteraction(props: Props){ const [selectedAlertComponents, setSelectedAlertComponents] = useState([]); const [validOptions, setValidOptions] = useState([]) const [validComponents, setValidComponents] = useState([]) + //condition value strings - so that decimals are easier to type into the field + const [valStrings, setValStrings] = useState(["0", "0"]); + const [numConditions, setNumConditions] = useState(0) + const [nodeOptions, setNodeOptions] = useState([]); + const [subtypeDropdown, setSubtypeDropdown] = useState(0); + const [nodeOne, setNodeOne] = useState(0) + const [nodeTwo, setNodeTwo] = useState(0) + const [maxConditions, setMaxConditions] = useState(2) + const [resultType, setResultType] = useState(quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT) + const [sinkMotor, setSinkMotor] = useState(false) + const [selectedSink, setSelectedSink] = useState("")//the key of the component to be the sink + const [sinkOptions, setSinkOptions] = useState([]) + const [resultMode, setResultMode] = useState(0) + const [resultValue, setResultValue] = useState("") + const [dutyCycleEnabled, setDutyCycleEnabled] = useState(false) + const [dutyCycle, setDutyCycle] = useState(""); + const [reporting, setReporting] = useState(true) + const [notify, setNotify] = useState(true) + //schedule variables + const [schedule, setSchedule] = useState( + pond.InteractionSchedule.create({ + weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] + }) + ); + + useEffect(() => { //if the device is passed in need to filter out possible components let validCompList: Component[] = [] let validOptions: Option[] = [] + let sinkOptions: Component[] = [] + + if(device){ + setMaxConditions(device.maxConditions()) + } linkedComponents.forEach((comp, key) => { - if(componentDevices.get(key) === device || !device){ + if(componentDevices.get(key) === device?.id() || !device){ + let ext = extension(comp.type(), comp.subType()) + if(ext.isController){ + sinkOptions.push(comp) + } validCompList.push(comp) } }) @@ -62,10 +145,59 @@ export default function NewObjectInteraction(props: Props){ setValidOptions(validOptions) setValidComponents(validCompList) + setSinkOptions(sinkOptions) },[device, linkedComponents, componentDevices]) + + //use the selected components to determine the lowest amount of nodes for the options + useEffect(() => { + setNodeOne(0); + setNodeTwo(0); + setSubtypeDropdown(0); + let nodeCount = 12; //start with the maximum number of nodes for a cable + validComponents.forEach(comp => { + if (selectedAlertComponents.includes(comp.key())) { + if (comp.lastMeasurement[0] && comp.lastMeasurement[0].values[0]) { + let nodes = comp.lastMeasurement[0].values[0].values.length; + nodeCount = nodes < nodeCount ? nodes : nodeCount; + } + } + }); + let options = []; + for (let i = 0; i <= nodeCount; i++) { + options.push( + + {i === 0 ? "None" : "Node " + i} + + ); + } + setNodeOptions(options); + }, [validComponents, selectedAlertComponents]); const close = (refresh: boolean) => { + //reset the state variables to their default values + setNewAlertComponentType(quack.ComponentType.COMPONENT_TYPE_INVALID); + setConditions([]); + setSelectedAlertComponents([]); + setValidOptions([]) + setValidComponents([]) + setValStrings(["0", "0"]); + setNumConditions(0) + setNodeOptions([]); + setSubtypeDropdown(0); + setNodeOne(0) + setNodeTwo(0) + setMaxConditions(2) + setResultType(quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT) + setSinkMotor(false) + setSelectedSink("")//the key of the component to be the sink + setSinkOptions([]) + setResultMode(0) + setResultValue("") + setDutyCycleEnabled(false) + setDutyCycle(""); + setReporting(true) + setNotify(true) onClose(refresh) } const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => { @@ -115,6 +247,758 @@ export default function NewObjectInteraction(props: Props){ ); }; + const measurementTypeMenuItems = () => { + return availableMeasurementTypes(newAlertComponentType).map(measurementType => ( + + {describeMeasurement(measurementType).label()} + + )); + }; + + const changeCondition = (newCondition: pond.InteractionCondition, index: number) => { + let c = conditions; + c[index] = newCondition; + setConditions([...c]); + }; + + const conditionGroup = (condition: pond.InteractionCondition, index: number) => { + let measurementType = condition.measurementType; + let describer = describeMeasurement(measurementType, newAlertComponentType); + let isBoolean = condition.measurementType === Measurement.boolean; + return ( + + + {index > 0 && ( + + AND + + )} + + { + condition.measurementType = +event.target.value; + changeCondition(condition, index); + }} + autoFocus={false} + margin="dense" + variant="standard" + fullWidth + InputLabelProps={{ shrink: true }}> + {measurementTypeMenuItems()} + + + + { + condition.comparison = +event.target.value; + changeCondition(condition, index); + }} + autoFocus={false} + margin="dense" + variant="standard" + fullWidth + InputLabelProps={{ shrink: true }}> + {!isBoolean && [ + + {"is below"} + , + + {"is above"} + + ]} + + {"is exactly"} + + + + + { + let strings = valStrings; + strings[index] = event.target.value; + setValStrings([...strings]); + if (!isNaN(+valStrings[index])) { + condition.value = describer.toStored(+event.target.value); + changeCondition(condition, index); + } + }} + autoFocus={false} + margin="dense" + variant="standard" + fullWidth + InputProps={{ + endAdornment: {describer.unit()} + }} + InputLabelProps={{ shrink: true }}> + {isBoolean && [ + + {describer.enumerations()[0]} + , + + {describer.enumerations()[1]} + + ]} + + + + + { + conditions.splice(index, 1) + setNumConditions(conditions.length) + }} + className={classNames(classes.redButton, classes.noPadding)}> + + + + + + {nodeOptions.length > 2 && ( + + + { + setSubtypeDropdown(+event.target.value); + }} + margin="dense" + variant="standard" + fullWidth + InputLabelProps={{ shrink: true }}> + + Any + + + Average + + + Single Node + + + Node Diff + + + Up To + + + + + {(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && ( + { + setNodeOne(+event.target.value); + }} + margin="dense" + variant="standard" + fullWidth + InputLabelProps={{ shrink: true }}> + {nodeOptions} + + )} + + + {subtypeDropdown === 3 && ( + { + setNodeTwo(+event.target.value); + }} + margin="dense" + variant="standard" + fullWidth + InputLabelProps={{ shrink: true }}> + {nodeOptions} + + )} + + + )} + + ); + }; + + const conditionInput = () => { + return ( + + Conditions + {selectedAlertComponents.length > 0 ? ( + + {conditions.map((condition, i) => conditionGroup(condition, i))} + + ) : ( + You must select a source before adding conditions + )} + + = maxConditions} + aria-label="Add another condition" + onClick={() => { + let newConditions = conditions.concat(initialConditions(newAlertComponentType)); + setConditions(newConditions); + setNumConditions(newConditions.length) + }} + className={classNames(classes.greenButton, classes.noPadding)}> + + + + + ); + }; + + const sinkSelector = () => { + return ( + { + setSelectedSink(e.target.value) + setSinkMotor(linkedComponents.get(e.target.value)?.type() === ComponentType.stepperMotor) + }} + fullWidth + autoFocus={false} + margin="dense" + variant="standard" + InputLabelProps={{ shrink: true }}> + + Select Controller + + {sinkOptions.map(comp => ( + + {comp.name()} + + ))} + + ); + } + + const describeSink = (): MeasurementDescriber => { + const sink = linkedComponents.get(selectedSink); + return describeMeasurement( + Measurement.boolean, + or(sink, Component.create()).settings.type, + or(sink, Component.create()).settings.subtype + ); + }; + + const dutyCycleInput = () => { + return ( + + + { + setDutyCycleEnabled(checked) + }} + value="enableDutyCycle" + color="secondary" + /> + } + label="Once every" + /> + + + { + setDutyCycle(e.target.value) + }} + fullWidth + autoFocus={false} + margin="dense" + variant="standard" + disabled={!dutyCycleEnabled} + error={isNaN(Number(dutyCycle)) || Number(dutyCycle) < 0} + InputLabelProps={{ + shrink: true + }} + InputProps={{ + endAdornment: seconds + }}> + + + ); + }; + + const notificationInput = () => { + return ( + + {resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT && ( + + { + setReporting(checked) + }} + value="notificationReports" + color="secondary" + /> + } + label="Report" + /> + + )} + + { + setNotify(checked) + }} + value="interactionNotification" + color="secondary" + /> + } + label="Notify" + disabled={!reporting} + /> + + {notify && ( + + + Everyone with notifications enabled for this device will receive an email and/or SMS + each time this result occurs + + + )} + + ); + }; + + const resultInput = () => { + return ( + + Result + + + { + setResultType(+e.target.value as quack.InteractionResultType) + }} + autoFocus={false} + margin="dense" + variant="standard" + fullWidth + InputLabelProps={{ shrink: true }}> + Report + Set + Toggle + Run + + + {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN && ( + + + {sinkSelector()} + + {sinkMotor && ( + + { + setResultMode(+e.target.value) + }} + fullWidth + autoFocus={false} + margin="dense" + variant="standard" + InputLabelProps={{ shrink: true }}> + + Clockwise + + + Counter Clockwise + + + + )} + + + for + + + + { + setResultValue(e.target.value) + }} + autoFocus={false} + margin="dense" + variant="standard" + fullWidth + InputProps={{ + endAdornment: sec + }} + InputLabelProps={{ shrink: true }} + /> + + + )} + {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET && ( + + + {sinkSelector()} + + {linkedComponents.get(selectedSink)?.type() !== + quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && ( + + + to + + + )} + {linkedComponents.get(selectedSink)?.type() !== + quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && ( + + { + setResultValue(e.target.value) + }} + fullWidth + autoFocus={false} + margin="dense" + variant="standard" + InputLabelProps={{ shrink: true }}> + {extension(linkedComponents.get(selectedSink)?.type() ?? quack.ComponentType.COMPONENT_TYPE_INVALID).states.map((state, i) => ( + + {state} + + ))} + + + )} + + )} + {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE && ( + + + {sinkSelector()} + + + { + if(checked){ + setResultValue('1') + }else{ + setResultValue('0') + } + }} + color="secondary" + /> + } + label={describeSink().enumerations()[isNaN(parseFloat(resultValue))?0:parseFloat(resultValue)]} + /> + + + )} + {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN && + dutyCycleInput()} + {notificationInput()} + + + ); + }; + + const toggleDaySelected = (day: string, event: any) => { + let updatedSchedule = cloneDeep(schedule); + if (event.target.checked) { + if (!updatedSchedule.weekdays.includes(day)) { + updatedSchedule.weekdays.push(day); + } + } else { + updatedSchedule.weekdays.splice(updatedSchedule.weekdays.indexOf(day), 1); + } + setSchedule(updatedSchedule); + }; + + const daySelector = (day: string, size: any) => { + //const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create()); + return ( + + toggleDaySelected(day, event)} + value={day} + color="secondary" + /> + } + label={capitalize(day.substr(0, 2))} + labelPlacement="bottom" + /> + + ); + }; + + const setScheduleTime = (id: string, event: any) => { + let updatedSchedule = cloneDeep(schedule); + if (id === "shortcut") { + let value = event.target.value; + if (value === "allDay") { + updatedSchedule.timeOfDayStart = "00:00"; + updatedSchedule.timeOfDayEnd = "24:00"; + } else if (value === "before") { + updatedSchedule.timeOfDayStart = "00:00"; + if (!updatedSchedule.timeOfDayEnd || updatedSchedule.timeOfDayEnd === "24:00") { + updatedSchedule.timeOfDayEnd = "23:59"; + } + } else if (value === "after") { + updatedSchedule.timeOfDayEnd = "24:00"; + if (updatedSchedule.timeOfDayStart === "00:00") { + updatedSchedule.timeOfDayStart = "00:01"; + } + } else { + if (updatedSchedule.timeOfDayStart === "00:00") { + updatedSchedule.timeOfDayStart = "00:01"; + } + if (updatedSchedule.timeOfDayEnd === "24:00") { + updatedSchedule.timeOfDayEnd = "23:59"; + } + } + } else { + let timeOfDay = + event + .hour() + .toString() + .padStart(2, "0") + + ":" + + event + .minute() + .toString() + .padStart(2, "0"); + if (id === "start") { + updatedSchedule.timeOfDayStart = timeOfDay; + } else if (id === "end") { + if (timeOfDay === "00:00") { + timeOfDay = "24:00"; + } + updatedSchedule.timeOfDayEnd = timeOfDay; + } + } + updatedSchedule.timezone = moment.tz.guess(); + setSchedule(updatedSchedule); + }; + + const scheduler = () => { + //let schedule = pond.InteractionSchedule.create(); + let timezone = moment.tz.guess(); + let todStart = "00:00".split(":"); + let todEnd = "23:59".split(":"); + let start = moment + .tz(timezone) + .startOf("day") + .add(todStart[0], "hours") + .add(todStart[1], "minutes") + .local(); + let end = moment + .tz(timezone) + .startOf("day") + .add(todEnd[0], "hours") + .add(todEnd[1], "minutes") + .local(); + let descriptor = timeOfDayDescriptor(schedule); + let showStart = descriptor === "after" || descriptor === "from"; + let showEnd = descriptor === "before" || descriptor === "from"; + let showBoth = showStart && showEnd; + return ( + + Schedule + + {daySelector("sunday", 3)} + {daySelector("monday", 3)} + {daySelector("tuesday", 3)} + {daySelector("wednesday", 3)} + {daySelector("thursday", 4)} + {daySelector("friday", 4)} + {daySelector("saturday", 4)} + + + + setScheduleTime("shortcut", event)} + fullWidth + autoFocus={false} + margin="normal" + variant="standard" + InputLabelProps={{ shrink: true }}> + + All Day + + + Before + + + After + + + From + + + + {showStart && ( + + } + value={start} + onChange={(event: any) => setScheduleTime("start", event)} + /> + + )} + {showBoth && ( + + + to + + + )} + {showEnd && ( + + } + value={end} + onChange={(event: any) => setScheduleTime("end", event)} + /> + + )} + + + ); + }; + + const addInteractionToComponents = () => { + //array of device and component ids ie, [4:9-5-12, 4:9-5-13, 3:9-5-12] + let compIds: string[] = []; + selectedAlertComponents.forEach(comp => { + let devID = componentDevices.get(comp); + let component = linkedComponents.get(comp); + if (devID && component) { + compIds.push(devID + ":" + component.locationString()); + } + }); + let newInteraction = pond.InteractionSettings.create(); + newInteraction.conditions = conditions; + newInteraction.instance = 1; + newInteraction.schedule = schedule; + newInteraction.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo); + newInteraction.sink = linkedComponents.get(selectedSink)?.location() + newInteraction.result = pond.InteractionResult.create({ + type: resultType, + mode: resultMode, + value: isNaN(parseFloat(resultValue)) ? 0 : parseFloat(resultValue), + dutyCycle: isNaN(parseFloat(dutyCycle)) ? 0 : parseFloat(dutyCycle) + }); + newInteraction.notifications = pond.InteractionNotifications.create({ + reports: reporting, + notify: reporting && notify + }); + interactionsAPI + .addInteractionToComponents(compIds, newInteraction, as) + .then(_resp => { + openSnack("interaction added to selected components"); + }) + .catch(_err => { + openSnack("there was a problem adding the interaction to the selected components"); + }); + }; + return ( ))} - {/* list of checkboxes for the components that match that type */} {listMatchingComponents()} - {/* have the conditions set here */} - {/* {conditionInput()} */} - {/* have the schedule set here */} - {/* {scheduler()} */} + {conditionInput()} + {device && resultInput()} + {scheduler()} - {/* */} + ) diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx index 51e3230..bec7c1b 100644 --- a/src/objects/objectInteractions/ObjectInteractions.tsx +++ b/src/objects/objectInteractions/ObjectInteractions.tsx @@ -20,11 +20,12 @@ interface Props { objectKey: string objectType: pond.ObjectType devices: Device[]; + permissions: pond.Permission[] refreshCallback?: () => {} } export default function ObjectInteractions(props: Props) { - const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback } = props + const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback, permissions } = props const [{as}] = useGlobalState() //list of alerts, each alert contains a list of interactions with matching conditions const [alerts, setAlerts] = useState([]); @@ -34,7 +35,7 @@ export default function ObjectInteractions(props: Props) { const [openNewInteraction, setOpenNewInteraction] = useState(false) const [allSinks, setAllSinks] = useState([]); // const [typeOptions, setTypeOptions] = useState([]); - const [selectedDevice, setSelectedDevice] = useState(undefined) + const [selectedDevice, setSelectedDevice] = useState(undefined) const matchConditions = (interaction: Interaction, set: Alert | Control) => { @@ -178,9 +179,7 @@ export default function ObjectInteractions(props: Props) { }); setAllSinks(sinkOptions); setAlerts(alerts); - console.log(alerts) setControls(controls); - console.log(controls) }).catch(err => { console.error("Interaction fetch error:", err) }) @@ -206,15 +205,19 @@ export default function ObjectInteractions(props: Props) { Interactions { - setSelectedDevice(deviceID) + addNew={(device) => { + setSelectedDevice(device) setOpenNewInteraction(true) }}/> - { + { setSelectedDevice(undefined) setOpenNewInteraction(true) - }}/> + }}/> diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 37c2d62..9fa36b6 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -50,11 +50,9 @@ import { GrainCable } from "models/GrainCable"; // import { Controller } from "models/Controller"; import { Pressure } from "models/Pressure"; import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material"; -import { getThemeType } from "theme"; import BinGraphs from "bin/graphs/BinGraphs"; import BinTour from "bin/BinTour"; import { getBinModel } from "common/DataImports/BinCables/BinCableImporter"; -import ObjectAlerts from "objects/ObjectAlerts"; import DevicePresetController from "device/presets/devicePresetController"; import { DevicePreset } from "models/DevicePreset"; import BinVisualizerV2 from "bin/BinVisualizerV2"; @@ -866,6 +864,7 @@ export default function Bin(props: Props) { devices={devices} objectKey={bin.key()} objectType={pond.ObjectType.OBJECT_TYPE_BIN} + permissions={permissions} /> )} @@ -1004,6 +1003,7 @@ export default function Bin(props: Props) { devices={devices} objectKey={bin.key()} objectType={pond.ObjectType.OBJECT_TYPE_BIN} + permissions={permissions} /> From c9d2eac6c364362f0e6014a93e7c3c20a3253039 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 1 Dec 2025 14:38:51 -0600 Subject: [PATCH 15/20] finished refactoring the object alerts into object interactions --- src/objects/objectInteractions/Alerts.tsx | 10 ++- src/objects/objectInteractions/Controls.tsx | 77 +++++++++++++++---- .../NewObjectInteraction.tsx | 2 + .../objectInteractions/Notifications.tsx | 6 +- .../objectInteractions/ObjectInteractions.tsx | 19 +++-- 5 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/objects/objectInteractions/Alerts.tsx b/src/objects/objectInteractions/Alerts.tsx index 0875a22..8546363 100644 --- a/src/objects/objectInteractions/Alerts.tsx +++ b/src/objects/objectInteractions/Alerts.tsx @@ -1,5 +1,5 @@ import { Add, ExpandMore } from "@mui/icons-material"; -import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material"; import { Component } from "models"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { pond, quack } from "protobuf-ts/pond"; @@ -71,7 +71,13 @@ export default function Alerts(props: Props){ Alerts {addNew && - {addNew()}} color="primary"> + } {alerts.map((alert, i) => ( diff --git a/src/objects/objectInteractions/Controls.tsx b/src/objects/objectInteractions/Controls.tsx index 1dc04ff..0346c87 100644 --- a/src/objects/objectInteractions/Controls.tsx +++ b/src/objects/objectInteractions/Controls.tsx @@ -1,7 +1,9 @@ -import { Add } from "@mui/icons-material"; -import { Accordion, AccordionDetails, AccordionSummary, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material"; -import { Component, Device } from "models"; -import { pond } from "protobuf-ts/pond"; +import { Add, ExpandMore } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Button, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material"; +import { Component, Device, Interaction } from "models"; +import { interactionResultText } from "pbHelpers/Interaction"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import { pond, quack } from "protobuf-ts/pond"; import React, { useEffect, useState } from "react"; export interface Control { @@ -39,11 +41,49 @@ export default function Controls(props: Props){ setDeviceControls(map) },[controls]) + const readableConditions = (control: Control) => { + let conditions: string = "" + let firstSource = control.sourceComponents[0] + control.conditions.forEach((condition, index) => { + let describer = describeMeasurement(condition.measurementType, firstSource.type(), firstSource.subType()); + let type = describer.label(); + let comparison = + condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO + ? "Exactly" + : condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN + ? "Above" + : "Below"; + let value = describer.toDisplay(condition.value); + conditions = conditions + (index !== 0 ? " and " : "") + type + " " + comparison + " " + value + describer.GetUnit() + }) + return conditions + }; + const controlAccordion = (control: Control) => { + let tempInteraction = Interaction.create() + tempInteraction.settings.result = control.result + let resultText = interactionResultText(tempInteraction, control.sinkComponent) + return ( - - Show what the control does - show the components that are doing it + + }> + + + {resultText}: + + + {readableConditions(control)} + + + + + + Controlling Components + {control.sourceComponents.map((comp, i) => ( + {comp.name()} + ))} + + ) } @@ -79,17 +119,20 @@ export default function Controls(props: Props){ - Alerts + Controls - { - setOpenDeviceMenu(true) - setAnchor(e.currentTarget) - }} - color="primary"> - - + {addNew && + + } {devices.map(device => ( diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx index 889e061..8e76c25 100644 --- a/src/objects/objectInteractions/NewObjectInteraction.tsx +++ b/src/objects/objectInteractions/NewObjectInteraction.tsx @@ -996,6 +996,8 @@ export default function NewObjectInteraction(props: Props){ }) .catch(_err => { openSnack("there was a problem adding the interaction to the selected components"); + }).finally(() => { + close(true) }); }; diff --git a/src/objects/objectInteractions/Notifications.tsx b/src/objects/objectInteractions/Notifications.tsx index aa2e410..6cc65e8 100644 --- a/src/objects/objectInteractions/Notifications.tsx +++ b/src/objects/objectInteractions/Notifications.tsx @@ -79,7 +79,11 @@ export default function Notifications(props: Props){ return ( - Notifications + + + Notifications + + {recentNotifications && ( {recentNotifications.map((notification, i) => ( diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx index bec7c1b..d50adb9 100644 --- a/src/objects/objectInteractions/ObjectInteractions.tsx +++ b/src/objects/objectInteractions/ObjectInteractions.tsx @@ -1,5 +1,5 @@ import { Box, Button, Card, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import Alerts, { Alert } from "./Alerts"; import Controls, { Control } from "./Controls"; import { Component, Device, Interaction } from "models"; @@ -21,11 +21,10 @@ interface Props { objectType: pond.ObjectType devices: Device[]; permissions: pond.Permission[] - refreshCallback?: () => {} } export default function ObjectInteractions(props: Props) { - const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback, permissions } = props + const { linkedComponents, componentDevices, devices, objectKey, objectType, permissions } = props const [{as}] = useGlobalState() //list of alerts, each alert contains a list of interactions with matching conditions const [alerts, setAlerts] = useState([]); @@ -86,8 +85,8 @@ export default function ObjectInteractions(props: Props) { } } - useEffect(() => { - const newInteractions: Interaction[] = []; + const load = useCallback(()=>{ +const newInteractions: Interaction[] = []; const interactionSourceMap = new Map(); const sinkOptions: Component[] = []; const alerts: Alert[] = []; @@ -183,7 +182,11 @@ export default function ObjectInteractions(props: Props) { }).catch(err => { console.error("Interaction fetch error:", err) }) - }, [linkedComponents, interactionsAPI, componentDevices, as]); + },[linkedComponents, interactionsAPI, componentDevices, as]) + + useEffect(() => { + load() + }, [load]); @@ -193,8 +196,8 @@ export default function ObjectInteractions(props: Props) { open={openNewInteraction} onClose={(refresh) => { setOpenNewInteraction(false) - if(refresh && refreshCallback){ - refreshCallback() + if(refresh){ + load() } }} linkedComponents={linkedComponents} From 633ef3817fb911faa6bf66698566a90a7cd7ddfb Mon Sep 17 00:00:00 2001 From: csawatzky Date: Wed, 3 Dec 2025 15:04:55 -0600 Subject: [PATCH 16/20] added some steps to the bin tour to reference each of the tabs, added some step to the signup tour to show some more pages and also added a way to change unit settings IN the tour step, now it is entirely their fault if they skip it, we cant make it any more in their face --- src/bin/BinTour.tsx | 156 ++++++++++++++++++++---- src/bin/BinVisualizerV2.tsx | 52 ++++---- src/common/ButtonGroup.tsx | 1 + src/pages/Bin.tsx | 78 ++++++++---- src/pages/SignupCallback.tsx | 229 ++++++++++++++++++++++++++++++++--- 5 files changed, 427 insertions(+), 89 deletions(-) diff --git a/src/bin/BinTour.tsx b/src/bin/BinTour.tsx index a685ada..e885527 100644 --- a/src/bin/BinTour.tsx +++ b/src/bin/BinTour.tsx @@ -1,6 +1,6 @@ // import { useAuth0 } from "@auth0/auth0-react"; import { Typography } from "@mui/material"; -import Tour from "common/Tour"; +import Tour, { TourStep } from "common/Tour"; import { random } from "lodash"; import moment from "moment"; import { useGlobalState, useSnackbar, useUserAPI } from "providers"; @@ -8,8 +8,13 @@ import React, { useEffect, useState } from "react"; import { Step } from "react-joyride"; // import Emoji from "react-emoji-render"; -export default function BinTour() { +interface Props { + setDetailTabState?: ( detail: "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions", buttonIndex: number) => void +} + +export default function BinTour(props: Props) { // const { userID } = useAuth0(); + const {setDetailTabState} = props const [{ user }, dispatch] = useGlobalState(); const userID = user.id() const { error } = useSnackbar(); @@ -35,8 +40,8 @@ export default function BinTour() { } }; - const getTourSteps = (): Step[] => { - let steps: Step[] = [ + const getTourSteps = (): TourStep[] => { + let steps: TourStep[] = [ { title: ( @@ -103,24 +108,12 @@ export default function BinTour() { disableBeacon: false }, { - title: "Graphs", + title: "View Other Data", content: ( - Bin related analytics are displayed here. - - - ), - target: "#tour-graphs", - placement: "left", - disableBeacon: false - }, - { - title: "Choose your graphs", - content: ( - - - Use this tab to view other sets of data. Sensors must be attached to view sensor data. + Use these tabs to view other Bin Details such as inventory levels over time, + alerts and interactions for connected components, and data for attached sensors ), @@ -128,6 +121,122 @@ export default function BinTour() { placement: "bottom", disableBeacon: false }, + { + title: "Inventory", + content: ( + + + The inventory tab shows your bins inventory level over a specified window as well as the times the bin mode was changed. + + + ), + target: "#tour-details", + placement: "left-start", + disableBeacon: false, + onNext: () => { + if (setDetailTabState) setDetailTabState("sensors", 1) + } + }, + { + title: "Sensors", + content: ( + + + The sensors tab shows readings from attached sensors over time + + + ), + target: "#tour-details", + placement: "left-start", + disableBeacon: false, + onNext: () => { + if (setDetailTabState) setDetailTabState("analytics", 2) + } + }, + { + title: "Analytics", + content: ( + + + The analysis tab shows analytic bin data using attached sensors + + + ), + target: "#tour-details", + placement: "left-start", + disableBeacon: false, + onNext: () => { + if (setDetailTabState) setDetailTabState("alerts", 3) + } + }, + { + title: "Alerts and Interactions", + content: ( + + + The Alerts tab allows you to view or add any new interactions or alerts for connected components + and displays recent notifications from those alerts + + + ), + target: "#tour-details", + placement: "left-start", + disableBeacon: false, + onNext: () => { + if (setDetailTabState) setDetailTabState("presets", 4) + } + }, + { + title: "Bin Mode Presets", + content: ( + + + The Presets tab allows you to create custom presets for the interactions for a device when changing bin modes + + + ), + target: "#tour-details", + placement: "left-start", + disableBeacon: false, + onNext: () => { + if (setDetailTabState) setDetailTabState("transactions", 5) + } + }, + { + title: "Bin Transactions", + content: ( + + + The transactions tab allows view any pending transaction when using a hybrid inventory control + + + ), + target: "#tour-details", + placement: "left-start", + disableBeacon: false, + onNext: () => { + if (setDetailTabState) setDetailTabState("inventory", 0) + } + }, + { + title: "Bin Conditions", + content: ( + + + This section will display conditions based on the bin mode + + + Storage Mode: simply displays the Storage conditions by itself with the target temperature and moisture set in the bin settings + + + Other Modes: will display the interactions for the bins devices that are controlling a fan or heater on the bin as well as the storage conditions + + + ), + target: "#tour-conditions", + placement: "right", + disableBeacon: false + }, { title: "Change Mode", content: ( @@ -138,12 +247,15 @@ export default function BinTour() {
  • Storage mode: default
  • - Drying mode: use heat to dry grain ☀️ + Cooldown mode: use fans to hold bin temperature lower ❆❅ + {/* */} +
  • +
  • + Drying mode: use heat to dry grain ☀️ (only visible when the target moisture is lower than the starting moisture in the bin settings) {/* */}
  • - Cooldown mode: use fans to hold bin temperature lower ❆❅ - {/* */} + Hydrating mode: use humid air to hydrate grain (only visible when the target moisture is higher than the starting moisture in the bin settings)
diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index 7d99671..baa27f6 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -1727,32 +1727,34 @@ export default function BinVisualizer(props: Props) { {modeChangeInProgress && } - { - setModeStorage() + + { + setModeStorage() + } + }, + { + title: "Cooldown", + function: () => { + setModeCooldown() + } + }, + { + title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying", + function: () => { + // setShowInputMoisture(true) + setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture + } } - }, - { - title: "Cooldown", - function: () => { - setModeCooldown() - } - }, - { - title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying", - function: () => { - // setShowInputMoisture(true) - setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture - } - } - ]} - toggledButtons={determineToggle(mode)} - toggle - /> + ]} + toggledButtons={determineToggle(mode)} + toggle + /> + ); }; diff --git a/src/common/ButtonGroup.tsx b/src/common/ButtonGroup.tsx index 3c2c64c..e326d64 100644 --- a/src/common/ButtonGroup.tsx +++ b/src/common/ButtonGroup.tsx @@ -57,6 +57,7 @@ interface Props { * When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data */ disableAll?: boolean + } const useStyles = makeStyles((theme: Theme) => { diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 9fa36b6..cc667d8 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -195,6 +195,7 @@ export default function Bin(props: Props) { const [headspaceCO2, setHeadspaceCO2] = useState([]); const [heaters, setHeaters] = useState([]); const [fans, setFans] = useState([]); + const [activeDetails, setActiveDetails] = useState([0]) const [detail, setDetail] = useState< "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions" >("inventory"); @@ -805,7 +806,7 @@ export default function Bin(props: Props) { {overview()} {preferences && binComponents(preferences)}
- + {(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING || bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING || bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && ( @@ -824,34 +825,53 @@ export default function Bin(props: Props) { - - - + + setDetail("inventory") + function: () => { + setDetail("inventory") + setActiveDetails([0]) + } }, { title: "Sensors", - function: () => setDetail("sensors") + function: () => { + setDetail("sensors") + setActiveDetails([1]) + } }, { title: "Analysis", - function: () => setDetail("analytics") + function: () => { + setDetail("analytics") + setActiveDetails([2]) + } }, { title: "Alerts", - function: () => setDetail("alerts") + function: () => { + setDetail("alerts") + setActiveDetails([3]) + } }, { title: "Presets", - function: () => setDetail("presets") + function: () => { + setDetail("presets") + setActiveDetails([4]) + } }, { title: "Transactions", - function: () => setDetail("transactions") + function: () => { + setDetail("transactions") + setActiveDetails([5]) + } } ]} /> @@ -880,23 +900,27 @@ export default function Bin(props: Props) { /> )} - {(detail === "inventory" || detail === "sensors" || detail === "analytics") && ( - - )} {detail === "transactions" && + + } + {(detail === "inventory" || detail === "sensors" || detail === "analytics") && ( + + + + )} @@ -1141,7 +1165,11 @@ export default function Bin(props: Props) { {objectTeams()} {deviceMenu()} - + { + setDetail(detail) + setActiveDetails([buttonIndex]) + }}/> ); } diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index a22dbfc..43c8c5b 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -1,13 +1,15 @@ import { useDeviceAPI, useMobile, useUserAPI } from "hooks"; import PageContainer from "./PageContainer"; import { useEffect, useState } from "react"; -import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Typography } from "@mui/material"; +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Grid2, MenuItem, TextField, Typography } from "@mui/material"; import { useBinAPI, useGlobalState, useTeamAPI } from "providers"; import { CheckCircleOutline } from "@mui/icons-material"; import { green } from "@mui/material/colors"; import Tour, { TourStep } from "common/Tour"; import Emoji from "react-emoji-render"; import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; +import { pond } from "protobuf-ts/pond"; +import { User } from "models"; // import { color } from "framer-motion"; // interface Props { @@ -21,7 +23,7 @@ export default function SignupCallback () { const deviceAPI = useDeviceAPI(); const binAPI = useBinAPI(); const isMobile = useMobile() - const [{ user }] = useGlobalState(); + const [globalState, dispatch] = useGlobalState(); const whiteLabel = getWhitelabel() const [loading, setLoading] = useState(false) @@ -35,6 +37,7 @@ export default function SignupCallback () { const [doneBins, setDoneBins] = useState(false) const [doneTeams, setDoneTeams] = useState(false) const [doneDevices, setDoneDevices] = useState(false) + const [user, setUser] = useState(globalState.user); useEffect(() => { setLoading(true) @@ -55,6 +58,124 @@ export default function SignupCallback () { return loading === false && doneBins && doneTeams && doneDevices } + const changePressureUnit = (value: any) => { + let updatedUser = User.clone(user) + let pressureUnit: pond.PressureUnit; + switch (value) { + case 1: + pressureUnit = pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS; + break; + case 2: + pressureUnit = pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER; + break; + default: + pressureUnit = pond.PressureUnit.PRESSURE_UNIT_UNKNOWN; + break; + } + updatedUser.settings.pressureUnit = pressureUnit; + setUser(updatedUser) + }; + + const changeTemperatureUnit = (value: any) => { + let updatedUser = User.clone(user); + let temperatureUnit: pond.TemperatureUnit; + switch (value) { + case 1: + temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS; + break; + case 2: + temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT; + break; + default: + temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_UNKNOWN; + break; + } + updatedUser.settings.temperatureUnit = temperatureUnit; + setUser(updatedUser); + }; + + const unitPreferences = () => { + const { pressureUnit, temperatureUnit, distanceUnit, grainUnit } = user.settings; + return ( + + + changePressureUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + + Kilopascals (kPa) + + + Inches of Water (iwg) + + + + + changeTemperatureUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + Celsius (°C) + + Fahrenheit (°F) + + + + + changeDistanceUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + Meters (m) + Feet (ft) + + + {IsAdaptiveAgriculture() && ( + + changeGrainUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + Bushels (bu) + Tonnes (mT) + + + )} + + ); + }; + const content = () => { return ( @@ -104,12 +225,17 @@ export default function SignupCallback () { content: ( <> - In the user menu, you can make changes to your profile, and adjust unit preferences. + In the user menu, you can make changes to your profile, such as adjusting unit preferences.
If you are part of a team, you can select it here as well. +
+ + Set your unit preferences here now, they can be changed later from this menu: + + {unitPreferences()} ), target: "#tour-user-menu", @@ -150,35 +276,104 @@ export default function SignupCallback () { target: "#tour-dashboard", placement: "right" }) - if (IsAdaptiveAgriculture()) steps.push({ - title: "Bins", + steps.push({ + title: "Tasks", content: ( <> - You can view your bins and their inventory here. + You can view and create tasks here and assign a start and end time for them.
- If you are viewing as a team, your team's bins will be displayed here instead. + If you are viewing as a team you will see the teams tasks, you can even assign tasks to team members. ), - target: "#tour-bins", - placement: "right", - disableBeacon: true, + target: "#tour-tasks", + placement: "right" }) + //tasks step + if (IsAdaptiveAgriculture()) { + steps.push({ + title: "Visual Farm", + content: ( + <> + + You can view your Visual Farm here, it will allow tou to draw fields and plot bins an a map. + +
+ + If you are viewing as a team, your team's fields and bins will be displayed here instead. + + + ), + target: "#tour-visual-farm", + placement: "right", + disableBeacon: true, + }) + steps.push({ + title: "Fields", + content: ( + <> + + You can view your fields you have drawn on the map here. + +
+ + If you are viewing as a team, your team's fields will be displayed here instead. + + + ), + target: "#tour-field-list", + placement: "right", + disableBeacon: true, + }) + steps.push({ + title: "Bins", + content: ( + <> + + You can view your bins and their inventory here. + +
+ + If you are viewing as a team, your team's bins will be displayed here instead. + + + ), + target: "#tour-bins", + placement: "right", + disableBeacon: true, + }) + steps.push({ + title: "Contracts", + content: ( + <> + + You can view and track any contracts you have created here. + +
+ + If you are viewing as a team, your team's contracts will be displayed here instead. + + + ), + target: "#tour-contracts", + placement: "right", + disableBeacon: true, + }) + } return steps; }; const endTour = () => { setIsTourRunning(false); - // if (user) { - // user.status.finishedBinIntro = moment().toJSON(); - // userAPI - // .updateUser(userID, user.protobuf()) - // .then(() => dispatch({ key: "user", value: user })) - // .catch((err: any) => error(err)); - // } + if (user) { + userAPI + .updateUser(user.id(), user.protobuf()) + .then(() => dispatch({ key: "user", value: user })) + .catch((err: any) => console.error(err)); + } }; const closeDialog = () => { From 2ad491c2895acae92d8ab533bdc0c17cd853f3e5 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 8 Dec 2025 15:29:50 -0600 Subject: [PATCH 17/20] adding caldwell fan model GGL-81011 --- src/fans/fans_client.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/fans/fans_client.json b/src/fans/fans_client.json index 58f4040..cdc21f4 100644 --- a/src/fans/fans_client.json +++ b/src/fans/fans_client.json @@ -3125,6 +3125,14 @@ "rpm": 1750, "diameter": 0, "horsepower": 30 + }, + { + "id": 445, + "name": "CALDWELL GGL-81011", + "kind": "centrifugal(low speed)", + "rpm": 0, + "diameter": 0, + "horsepower": 10 } ] } From 2edca58f7c69475891a622c0b094a5b99300ba63 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 18 Dec 2025 15:16:10 -0600 Subject: [PATCH 18/20] updating the version check in the device model for firmware versions, the string comparison was just wrong --- src/device/DeviceWizard.tsx | 1 - src/models/Device.ts | 75 +++++++++++++++++++++++++++++++------ src/pages/Device.tsx | 1 - 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/device/DeviceWizard.tsx b/src/device/DeviceWizard.tsx index cf0a061..3bb7fb9 100644 --- a/src/device/DeviceWizard.tsx +++ b/src/device/DeviceWizard.tsx @@ -226,7 +226,6 @@ export default function DeviceWizard(props: Props) { */ const adjustAvailablePositions = (port: PortInformation, availability: DeviceAvailabilityMap) => { let currentAvailability = availability; - console.log(port) switch (port.addressType) { case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY: currentAvailability.set(port.addressType, [{ address: port.address, label: port.label }]); diff --git a/src/models/Device.ts b/src/models/Device.ts index 6ee074d..f4ea4ee 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -181,6 +181,59 @@ export class Device { return max } + private versionComparison(deviceVersion: string, requiredVersion: string): boolean { + const parse = (v: string) => { + const [core, pre] = v.split("-"); + const parts = core.split(".").map(n => parseInt(n, 10)); + + let preLabel = ""; + let preNum = 0; + + if (pre) { + const match = pre.match(/^([a-zA-Z]+)(\d*)$/); + if (match) { + preLabel = match[1].toLowerCase(); + preNum = match[2] ? parseInt(match[2], 10) : 0; + } + } + + return { parts, preLabel, preNum }; + }; + + const rank = (label: string): number => { + switch (label) { + case "alpha": return 1; + case "beta": return 2; + case "rc": return 3; + default: return 0; // release + } + }; + + const a = parse(deviceVersion); + const b = parse(requiredVersion); + + // Compare major/minor/patch + const len = Math.max(a.parts.length, b.parts.length); + for (let i = 0; i < len; i++) { + const av = a.parts[i] ?? 0; + const bv = b.parts[i] ?? 0; + if (av > bv) return true; + if (av < bv) return false; + } + + // Handle prerelease vs release + if (!a.preLabel && b.preLabel) return true; // release > prerelease + if (a.preLabel && !b.preLabel) return false; + + // Both prereleases → compare rank + if (a.preLabel !== b.preLabel) { + return rank(a.preLabel) >= rank(b.preLabel); + } + + // Same prerelease label → compare numeric suffix + return a.preNum >= b.preNum; + } + public featureSupported(feature: string): boolean { let versions = featureVersions.get(feature); if (!versions) { @@ -188,27 +241,27 @@ export class Device { } switch (this.settings.platform) { case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON: - return this.status.firmwareVersion >= versions.photon; + return this.versionComparison(this.status.firmwareVersion, versions.photon); case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON: - return this.status.firmwareVersion >= versions.electron; + return this.versionComparison(this.status.firmwareVersion, versions.electron); case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR: - return this.status.firmwareVersion >= versions.v2Cell; + return this.versionComparison(this.status.firmwareVersion, versions.v2Cell); case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI: - return this.status.firmwareVersion >= versions.v2Wifi; + return this.versionComparison(this.status.firmwareVersion, versions.v2Wifi); case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3: - return this.status.firmwareVersion >= versions.v2WifiS3; + return this.versionComparison(this.status.firmwareVersion, versions.v2WifiS3); case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3: - return this.status.firmwareVersion >= versions.v2CellS3; + return this.versionComparison(this.status.firmwareVersion, versions.v2CellS3); case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK: - return this.status.firmwareVersion >= versions.v2CellBlack; + return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlack); case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN: - return this.status.firmwareVersion >= versions.v2CellGreen; + return this.versionComparison(this.status.firmwareVersion, versions.v2CellGreen); case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE: - return this.status.firmwareVersion >= versions.v2WifiBlue; + return this.versionComparison(this.status.firmwareVersion, versions.v2WifiBlue); case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE: - return this.status.firmwareVersion >= versions.v2CellBlue; + return this.versionComparison(this.status.firmwareVersion, versions.v2CellBlue); case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE: - return this.status.firmwareVersion >= versions.v2EthBlue; + return this.versionComparison(this.status.firmwareVersion, versions.v2EthBlue); default: return false; } diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 7344659..ef9b673 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -169,7 +169,6 @@ export default function DevicePage() { const loadPortScan = () => { deviceAPI.listFoundComponents(parseInt(deviceID)) .then(resp => { - console.log(resp.data) if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){ setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined) } From 7964aa09cc3b53f4512b2792fb81e9c2f67d5b52 Mon Sep 17 00:00:00 2001 From: csawatzky Date: Mon, 29 Dec 2025 16:23:02 -0600 Subject: [PATCH 19/20] added optional custom card function to the binslist so that on the map page we can have the map move to the bin and open the bin drawer when the card is clicked from the yard drawer --- src/bin/BinsList.tsx | 20 ++++++++++++++++--- src/bin/BinyardDisplay.tsx | 10 +++++++++- src/maps/mapDrawers/BinYardDrawer.tsx | 11 ++++++++-- .../mapObjectControllers/AgMapController.tsx | 12 +++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/bin/BinsList.tsx b/src/bin/BinsList.tsx index 483df50..ad7ae64 100644 --- a/src/bin/BinsList.tsx +++ b/src/bin/BinsList.tsx @@ -23,6 +23,12 @@ interface Props { //startingTranslate: number; valDisplay?: "high" | "low" | "average"; insert?: boolean + /** + * optional function for when you want clicking the card to do something other than navigate to the bin page + * @param bin the bin model for the card that was clicked + * @returns void + */ + cardClickFunction?: (bin: Bin) => void } const useStyles = makeStyles((_theme) => { @@ -40,7 +46,7 @@ const useStyles = makeStyles((_theme) => { }); export default function BinsList(props: Props) { - const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert } = props; + const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert, cardClickFunction } = props; const classes = useStyles(); // const history = useHistory(); const navigate = useNavigate() @@ -53,6 +59,14 @@ export default function BinsList(props: Props) { navigate(path, { state: {bin: bins[i]} }); }; + const cardClicked = (index: number) => { + if(cardClickFunction) { + cardClickFunction(bins[index]) + }else{ + goToBin(index) + } + } + const leftArrow = () => { const visibility = useContext(VisibilityContext); //const isFirstVisible = visibility.useIsVisible('first', false) @@ -95,7 +109,7 @@ export default function BinsList(props: Props) { key={i} className={classes.gridListTile} onClick={() => { - !duplicate && goToBin(i) + !duplicate && cardClicked(i) }}> { - !duplicate && goToBin(i) + !duplicate && cardClicked(i) }}> void } const useStyles = makeStyles((theme: Theme) => ({ @@ -82,7 +87,7 @@ const useStyles = makeStyles((theme: Theme) => ({ })); export default function BinyardDisplay(props: Props) { - const { yardKey, insert } = props; + const { yardKey, insert, binClicked } = props; const classes = useStyles(); const binAPI = useBinAPI(); const theme = useTheme(); @@ -321,6 +326,7 @@ export default function BinyardDisplay(props: Props) { bins={grainBins} duplicateBin={duplicateBin} title={"Grain Bins"} + cardClickFunction={binClicked} />
@@ -338,6 +344,7 @@ export default function BinyardDisplay(props: Props) { bins={fertBins} duplicateBin={duplicateBin} title={"Fertilizer Bins"} + cardClickFunction={binClicked} /> @@ -355,6 +362,7 @@ export default function BinyardDisplay(props: Props) { bins={emptyBins} duplicateBin={duplicateBin} title={"Empty Bins"} + cardClickFunction={binClicked} // loadMore={newTranslation => { // //only triggered in the scroll view so this will never trigger in grid view // if (yardBins.length < binTotal) { diff --git a/src/maps/mapDrawers/BinYardDrawer.tsx b/src/maps/mapDrawers/BinYardDrawer.tsx index 3153901..e347014 100644 --- a/src/maps/mapDrawers/BinYardDrawer.tsx +++ b/src/maps/mapDrawers/BinYardDrawer.tsx @@ -1,6 +1,7 @@ import BinyardDisplay from "bin/BinyardDisplay"; import DisplayDrawer from "common/DisplayDrawer"; import MapMarkerSettings from "maps/MapMarkerSettings"; +import { Bin } from "models"; //import Bins from "pages/Bins"; import { pond } from "protobuf-ts/pond"; import { useBinYardAPI, useGlobalState, useSnackbar } from "providers"; @@ -14,10 +15,11 @@ interface Props { removeMarker: (key: string) => void; updateMarker: (key: string, objectSettings: pond.BinYardSettings) => void; moveMap: (long: number, lat: number) => void; + binClicked: (bin: Bin) => void } export default function BinYardDrawer(props: Props) { - const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap } = props; + const { open, onClose, selectedYard, yards, removeMarker, updateMarker, moveMap, binClicked } = props; const [{as}] = useGlobalState(); const [yard, setYard] = useState(pond.BinYardSettings.create()); const [openMarkerSettings, setOpenMarkerSettings] = useState(false); @@ -85,7 +87,12 @@ export default function BinYardDrawer(props: Props) { const drawerBody = () => { //return ; - return ; + return ( + + ) }; /** diff --git a/src/maps/mapObjectControllers/AgMapController.tsx b/src/maps/mapObjectControllers/AgMapController.tsx index 752992d..71e91a5 100644 --- a/src/maps/mapObjectControllers/AgMapController.tsx +++ b/src/maps/mapObjectControllers/AgMapController.tsx @@ -1659,6 +1659,18 @@ import { Result } from "@mapbox/mapbox-gl-geocoder"; { + //move the map to the correct bin location, assuming the bin was plotted on the map + let location = bin.location() + if(location !== null && location !== undefined){ + moveMap(location.latitude, location.longitude, zoomLevels.close, isMobile) + } + //close the yarddrawer + setYardDrawer(false) + //open the bin drawer using the correct bin + setObjectKey(bin.key()) + setBinDrawer(true) + }} onClose={() => { if (new Date().valueOf() > endTime) { setYardDrawer(false); From dd46c2ece368eab6fdb53610ad09778d21784f6a Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 6 Jan 2026 11:25:15 -0600 Subject: [PATCH 20/20] merged staging and switched to master proto --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fc7a1ac..ee0e9b7 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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",