merged master and accepted both changes
This commit is contained in:
commit
1ed355d5a8
35 changed files with 2153 additions and 753 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -43,7 +43,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#master",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||
"query-string": "^9.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
|
|
@ -10960,7 +10960,7 @@
|
|||
},
|
||||
"node_modules/protobuf-ts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c08965a296f2cd0799472fd8b163186cf6885d4c",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#3f647071f211c4552c2acc23a8ecabb6904a4156",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { CssBaseline, Theme } from '@mui/material'
|
|||
import { AppThemeProvider } from 'theme/AppThemeProvider'
|
||||
import HTTPProvider from 'providers/http'
|
||||
import { Crisp } from "crisp-sdk-web";
|
||||
import { useSnackbar } from 'hooks'
|
||||
// import FirmwareLoader from './FirmwareLoader'
|
||||
|
||||
const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => {
|
||||
|
|
@ -65,6 +66,7 @@ export default function UserWrapper(props: Props) {
|
|||
const useAuth = useAuth0();
|
||||
const hasFetched = useRef(false);
|
||||
const [global, setGlobal] = useState<undefined | GlobalState>(undefined)
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
const user_id = or(useAuth.user?.sub, "")
|
||||
|
||||
|
|
@ -85,7 +87,8 @@ export default function UserWrapper(props: Props) {
|
|||
backgroundTasksComplete: false,
|
||||
firmware: new Map()
|
||||
})
|
||||
}).catch(() => {
|
||||
}).catch((err) => {
|
||||
snackbar.error("get user and team: "+err)
|
||||
userAPI.getUser(user_id).then(user => {
|
||||
setGlobal({
|
||||
user: user ? user : User.create(),
|
||||
|
|
@ -96,7 +99,8 @@ export default function UserWrapper(props: Props) {
|
|||
backgroundTasksComplete: false,
|
||||
firmware: new Map()
|
||||
})
|
||||
}).catch(() => {
|
||||
}).catch((err) => {
|
||||
snackbar.error("get user: "+err)
|
||||
setGlobal(globalDefault)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -261,32 +261,39 @@ export default function BinCard(props: Props) {
|
|||
let val;
|
||||
let useEMC = false;
|
||||
|
||||
switch (valDisplay) {
|
||||
case "average":
|
||||
//taking the switch cases out so that the node always displays the average for the moisture/humidity
|
||||
if (average?.emc) {
|
||||
useEMC = true;
|
||||
val = average.emc;
|
||||
} else {
|
||||
val = average?.humid;
|
||||
}
|
||||
break;
|
||||
case "low":
|
||||
if (coldNode?.emc) {
|
||||
useEMC = true;
|
||||
val = coldNode.emc;
|
||||
} else {
|
||||
val = coldNode?.humid;
|
||||
}
|
||||
break;
|
||||
case "high":
|
||||
if (hotNode?.emc) {
|
||||
useEMC = true;
|
||||
val = hotNode.emc;
|
||||
} else {
|
||||
val = hotNode?.humid;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// switch (valDisplay) {
|
||||
// case "average":
|
||||
// if (average?.emc) {
|
||||
// useEMC = true;
|
||||
// val = average.emc;
|
||||
// } else {
|
||||
// val = average?.humid;
|
||||
// }
|
||||
// break;
|
||||
// case "low":
|
||||
// if (coldNode?.emc) {
|
||||
// useEMC = true;
|
||||
// val = coldNode.emc;
|
||||
// } else {
|
||||
// val = coldNode?.humid;
|
||||
// }
|
||||
// break;
|
||||
// case "high":
|
||||
// if (hotNode?.emc) {
|
||||
// useEMC = true;
|
||||
// val = hotNode.emc;
|
||||
// } else {
|
||||
// val = hotNode?.humid;
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
|
||||
if (val !== undefined && !isNaN(val)) {
|
||||
display = val.toFixed(2);
|
||||
|
|
@ -332,7 +339,8 @@ export default function BinCard(props: Props) {
|
|||
color="textSecondary"
|
||||
noWrap
|
||||
style={{ fontSize: "0.5rem" }}>
|
||||
{valDisplay === "high" ? "High" : valDisplay === "low" ? "Low" : "Average"}
|
||||
{/* {valDisplay === "high" ? "High" : valDisplay === "low" ? "Low" : "Average"} */}
|
||||
Average
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
<React.Fragment>
|
||||
|
|
@ -103,24 +108,12 @@ export default function BinTour() {
|
|||
disableBeacon: false
|
||||
},
|
||||
{
|
||||
title: "Graphs",
|
||||
title: "View Other Data",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
Bin related analytics are displayed here.
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-graphs",
|
||||
placement: "left",
|
||||
disableBeacon: false
|
||||
},
|
||||
{
|
||||
title: "Choose your graphs",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
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
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
|
|
@ -128,6 +121,122 @@ export default function BinTour() {
|
|||
placement: "bottom",
|
||||
disableBeacon: false
|
||||
},
|
||||
{
|
||||
title: "Inventory",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
The inventory tab shows your bins inventory level over a specified window as well as the times the bin mode was changed.
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-details",
|
||||
placement: "left-start",
|
||||
disableBeacon: false,
|
||||
onNext: () => {
|
||||
if (setDetailTabState) setDetailTabState("sensors", 1)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Sensors",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
The sensors tab shows readings from attached sensors over time
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-details",
|
||||
placement: "left-start",
|
||||
disableBeacon: false,
|
||||
onNext: () => {
|
||||
if (setDetailTabState) setDetailTabState("analytics", 2)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Analytics",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
The analysis tab shows analytic bin data using attached sensors
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-details",
|
||||
placement: "left-start",
|
||||
disableBeacon: false,
|
||||
onNext: () => {
|
||||
if (setDetailTabState) setDetailTabState("alerts", 3)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Alerts and Interactions",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
The Alerts tab allows you to view or add any new interactions or alerts for connected components
|
||||
and displays recent notifications from those alerts
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-details",
|
||||
placement: "left-start",
|
||||
disableBeacon: false,
|
||||
onNext: () => {
|
||||
if (setDetailTabState) setDetailTabState("presets", 4)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Bin Mode Presets",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
The Presets tab allows you to create custom presets for the interactions for a device when changing bin modes
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-details",
|
||||
placement: "left-start",
|
||||
disableBeacon: false,
|
||||
onNext: () => {
|
||||
if (setDetailTabState) setDetailTabState("transactions", 5)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Bin Transactions",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
The transactions tab allows view any pending transaction when using a hybrid inventory control
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-details",
|
||||
placement: "left-start",
|
||||
disableBeacon: false,
|
||||
onNext: () => {
|
||||
if (setDetailTabState) setDetailTabState("inventory", 0)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Bin Conditions",
|
||||
content: (
|
||||
<React.Fragment>
|
||||
<Typography variant="body2" paragraph>
|
||||
This section will display conditions based on the bin mode
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
Storage Mode: simply displays the Storage conditions by itself with the target temperature and moisture set in the bin settings
|
||||
</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
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
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
),
|
||||
target: "#tour-conditions",
|
||||
placement: "right",
|
||||
disableBeacon: false
|
||||
},
|
||||
{
|
||||
title: "Change Mode",
|
||||
content: (
|
||||
|
|
@ -138,12 +247,15 @@ export default function BinTour() {
|
|||
<ul>
|
||||
<li>Storage mode: default</li>
|
||||
<li>
|
||||
Drying mode: use heat to dry grain ☀️
|
||||
Cooldown mode: use fans to hold bin temperature lower ❆❅
|
||||
{/* <Emoji text=" ❆❅" /> */}
|
||||
</li>
|
||||
<li>
|
||||
Drying mode: use heat to dry grain ☀️ (only visible when the target moisture is lower than the starting moisture in the bin settings)
|
||||
{/* <Emoji text=" ☀️" /> */}
|
||||
</li>
|
||||
<li>
|
||||
Cooldown mode: use fans to hold bin temperature lower ❆❅
|
||||
{/* <Emoji text=" ❆❅" /> */}
|
||||
Hydrating mode: use humid air to hydrate grain (only visible when the target moisture is higher than the starting moisture in the bin settings)
|
||||
</li>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
|
|
|
|||
|
|
@ -1727,6 +1727,7 @@ export default function BinVisualizer(props: Props) {
|
|||
{modeChangeInProgress &&
|
||||
<CircularProgress color="primary" size={25}/>
|
||||
}
|
||||
<Box id="tour-bin-mode">
|
||||
<ButtonGroup
|
||||
disableAll={modeChangeInProgress}
|
||||
buttons={[
|
||||
|
|
@ -1754,6 +1755,7 @@ export default function BinVisualizer(props: Props) {
|
|||
toggle
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}>
|
||||
<BinCardV2
|
||||
bin={b}
|
||||
|
|
@ -129,7 +143,7 @@ export default function BinsList(props: Props) {
|
|||
key={i}
|
||||
className={classes.gridListTile}
|
||||
onClick={() => {
|
||||
!duplicate && goToBin(i)
|
||||
!duplicate && cardClicked(i)
|
||||
}}>
|
||||
<BinCardV2
|
||||
bin={b}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ import BinsList from "./BinsList";
|
|||
interface Props {
|
||||
yardKey: string;
|
||||
insert?: boolean;
|
||||
/**
|
||||
* function to run when a bin card is clicked, when not passed in will default to attempting to navigate to the bin page by adding the bin key to the url
|
||||
* @param bin the selected bin model
|
||||
*/
|
||||
binClicked?: (bin: Bin) => 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}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
|
|
@ -338,6 +344,7 @@ export default function BinyardDisplay(props: Props) {
|
|||
bins={fertBins}
|
||||
duplicateBin={duplicateBin}
|
||||
title={"Fertilizer Bins"}
|
||||
cardClickFunction={binClicked}
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -141,6 +141,28 @@ export default function DeviceWizard(props: Props) {
|
|||
</MenuItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedPort &&
|
||||
selectedPort.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY &&
|
||||
device.featureSupported("detectOneWire") &&
|
||||
selectedPort.autoDetectable && (
|
||||
<Tooltip title="Used to tell the Device to scan this port for any plugged in sensors">
|
||||
<MenuItem
|
||||
key={"scanOneWire"}
|
||||
disabled={scanInProgress || buttonClicked}
|
||||
onClick={() => {
|
||||
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
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
{portComponents.length > 0 && (
|
||||
<List>
|
||||
<ListSubheader>Current Components</ListSubheader>
|
||||
|
|
@ -204,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 }]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -31,17 +33,19 @@ export default function DeviceScannedComponents(props: Props){
|
|||
const compAPI = useComponentAPI();
|
||||
const deviceAPI = useDeviceAPI()
|
||||
const [scannedI2C, setScannedI2C] = useState<pond.DetectI2C>() //the unmodified scan of addresses
|
||||
const [validCompAddresses, setValidComponentAddresses] = useState<quack.AddressData[]>([]) //the filtered array of address data
|
||||
const [scannedOneWire, setScannedOneWire] = useState<pond.DetectOneWire[]>([])
|
||||
const [validI2CCompAddresses, setValidI2CComponentAddresses] = useState<quack.AddressData[]>([]) //the filtered array of address data
|
||||
const [components, setComponents] = useState<Component[]>([])
|
||||
const [steps, setSteps] = useState<CompStep[]>([]);
|
||||
const { error, success } = useSnackbar();
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [settingsValid, setSettingsValid] = useState(false);
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
const [clearOpen, setClearOpen] = 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 +80,7 @@ export default function DeviceScannedComponents(props: Props){
|
|||
}
|
||||
})
|
||||
}
|
||||
setValidComponentAddresses(valid)
|
||||
setValidI2CComponentAddresses(valid)
|
||||
},[scannedI2C])
|
||||
|
||||
const stepper = () => {
|
||||
|
|
@ -200,11 +204,12 @@ export default function DeviceScannedComponents(props: Props){
|
|||
})
|
||||
})
|
||||
}
|
||||
console.log(cloneList)
|
||||
setComponents(cloneList)
|
||||
}
|
||||
|
||||
const removeScan = (key: string) => {
|
||||
deviceAPI.removeFoundComponents(device.settings.deviceId, key)
|
||||
const removeScan = (key: string, type: pond.ObjectType) => {
|
||||
deviceAPI.removeFoundComponents(device.settings.deviceId, key, type)
|
||||
.then(resp => {
|
||||
console.log("Cleared this scan")
|
||||
}).catch(err => {
|
||||
|
|
@ -212,20 +217,51 @@ export default function DeviceScannedComponents(props: Props){
|
|||
})
|
||||
}
|
||||
|
||||
const removeAllScans = () => {
|
||||
deviceAPI.removeAllFoundComponents(device.settings.deviceId)
|
||||
.then(resp => {
|
||||
console.log("Cleared all scans")
|
||||
refreshCallback()
|
||||
}).catch(err => {
|
||||
console.log("There was a problem clearing scans")
|
||||
})
|
||||
}
|
||||
|
||||
const clearWarning = () => {
|
||||
return (
|
||||
<ResponsiveDialog open={clearOpen} onClose={()=>{setClearOpen(false)}}>
|
||||
<DialogTitle>Clear All Scans</DialogTitle>
|
||||
<DialogContent>This action will clear all scans for this device.</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={()=>{setClearOpen(false)}}>Close</Button>
|
||||
<Button variant="contained" color="primary" onClick={()=>{
|
||||
removeAllScans()
|
||||
setClearOpen(false)
|
||||
}}>Continue</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{clearWarning()}
|
||||
<Card raised sx={{padding: 1}}>
|
||||
<Box display="flex" flexDirection="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography sx={{fontWeight: 650, fontSize: 25}}>Sensor Scan</Typography>
|
||||
<Button variant="contained" color="primary" onClick={()=>{setClearOpen(true)}}>Clear</Button>
|
||||
</Box>
|
||||
{scannedI2C !== undefined &&
|
||||
<React.Fragment>
|
||||
<Box marginBottom={5}>
|
||||
<Box justifyContent="space-between" display="flex">
|
||||
<Typography sx={{fontWeight: 650}}>
|
||||
I2C Sensors
|
||||
</Typography>
|
||||
<Button variant="contained" color="primary" onClick={()=>{removeScan(scannedI2C.key)}}>Clear Scan</Button>
|
||||
<Button variant="contained" color="primary" onClick={()=>{removeScan(scannedI2C.key, pond.ObjectType.OBJECT_TYPE_DETECT_I2C)}}>Clear Scan</Button>
|
||||
</Box>
|
||||
{validCompAddresses.length > 0 ? validCompAddresses.map((addr, index) => {
|
||||
{validI2CCompAddresses.length > 0 ? validI2CCompAddresses.map((addr, index) => {
|
||||
return (
|
||||
<ScannedI2C key={index} sensorNum={index+1} addressData={addr} deviceProduct={device.settings.product} componentSelectionCallback={componentSelection} availablePositions={availablePositions.get(quack.AddressType.ADDRESS_TYPE_I2C) ?? new Map<quack.ComponentType, number[]>()}/>
|
||||
)
|
||||
|
|
@ -240,7 +276,39 @@ export default function DeviceScannedComponents(props: Props){
|
|||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
</React.Fragment>
|
||||
</Box>
|
||||
}
|
||||
{scannedOneWire.length > 0 &&
|
||||
<Box>
|
||||
<Box justifyContent="space-between" display="flex">
|
||||
<Typography sx={{fontWeight: 650}}>
|
||||
Pin Port Sensors
|
||||
</Typography>
|
||||
</Box>
|
||||
{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(
|
||||
<Box key={i} paddingX={2}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems={"center"}>
|
||||
<Typography>Port: {portLabel}</Typography>
|
||||
<Button variant="contained" color="primary" onClick={()=>{
|
||||
removeScan(scan.key, pond.ObjectType.OBJECT_TYPE_DETECT_ONEWIRE)
|
||||
}}>Clear Port Scan</Button>
|
||||
</Box>
|
||||
<ScannedOneWirePort scan={scan} componentSelectionCallback={componentSelection}/>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
}
|
||||
<Box display="flex" flexDirection="row-reverse" marginTop={2}>
|
||||
<Button disabled={components.length === 0} variant="contained" color="primary" onClick={()=>{setOpenDialog(true)}}>Add Components</Button>
|
||||
|
|
|
|||
150
src/device/autoDetect/OneWire/PortComponent.tsx
Normal file
150
src/device/autoDetect/OneWire/PortComponent.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { Box, Checkbox, Grid2, MenuItem, TextField, Tooltip, Typography } from "@mui/material"
|
||||
import { useMobile } from "hooks"
|
||||
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, componentSelect} = props
|
||||
// const [selectedSubtype, setSelectedSubtype] = useState<number>(0)
|
||||
const [subtypeVal, setSubtypeVal] = useState("no subtype")//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<Map<string, number>>(new Map())
|
||||
const [subtypeOptions, setSubtypeOptions] = useState<Subtype[]>([])
|
||||
const [added, setAdded] = useState(false)
|
||||
const isMobile = useMobile()
|
||||
|
||||
useEffect(()=>{
|
||||
// setSelectedSubtype(compData.subtype)
|
||||
let validSubtypes: Subtype[] = []
|
||||
let subMap: Map<string, number> = new Map()
|
||||
let subVal: string = "no subtype"
|
||||
getSubtypes(compData.type).forEach(option => {
|
||||
if(compData.subtype === option.key || compData.subtype === 0){
|
||||
validSubtypes.push(option)
|
||||
if(subVal === "no subtype"){
|
||||
subVal = option.friendlyName
|
||||
}
|
||||
}
|
||||
subMap.set(option.friendlyName, option.key)
|
||||
})
|
||||
setSubtypeOptions(validSubtypes)
|
||||
setSubtypeMap(subMap)
|
||||
setSubtypeVal(subVal)
|
||||
},[compData])
|
||||
|
||||
const subtypeSelector = () => {
|
||||
return (
|
||||
<TextField
|
||||
value={subtypeVal}
|
||||
disabled={added}
|
||||
fullWidth
|
||||
onChange={(event) => {
|
||||
setSubtypeVal(event.target.value)
|
||||
// setSelectedSubtype(subtypeMap.get(event.target.value) ?? 0)
|
||||
compData.subtype = subtypeMap.get(event.target.value) ?? 0
|
||||
}}
|
||||
select>
|
||||
<MenuItem key={-1} value={"no subtype"}>Select the Component Subtype</MenuItem>
|
||||
{subtypeOptions.map((s, i) => (
|
||||
<MenuItem key={i} value={s.friendlyName}>{s.friendlyName}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
)
|
||||
}
|
||||
|
||||
const selectCompCheckbox = () => {
|
||||
return (
|
||||
<Tooltip
|
||||
title="Add to list to be added to the device"
|
||||
children={
|
||||
<Checkbox
|
||||
value={added}
|
||||
onChange={(_, checked) => {
|
||||
setAdded(checked)
|
||||
componentSelect(checked, compData)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const desktopDisplay = () => {
|
||||
return (
|
||||
<Box padding={2}>
|
||||
<Grid2 container spacing={2} direction="row" wrap="nowrap" alignContent="center" alignItems="center" justifyContent="space-between">
|
||||
<Grid2 size={3}>
|
||||
<Typography>
|
||||
{getFriendlyName(compData.type)}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2 size={3}>
|
||||
{subtypeOptions.length < 2
|
||||
?
|
||||
<Typography>
|
||||
{subtypeVal}
|
||||
</Typography>
|
||||
:
|
||||
subtypeSelector()}
|
||||
</Grid2>
|
||||
<Grid2 size={2}>
|
||||
<Typography>
|
||||
Cable ID: {compData.cableId}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2 size={2}>
|
||||
<Typography>
|
||||
Nodes: {compData.nodeCount}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2 size={1}>
|
||||
{selectCompCheckbox()}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const mobileDisplay = () => {
|
||||
return(
|
||||
<Box padding={2}>
|
||||
<Grid2 container spacing={2} direction="row" wrap="nowrap" alignContent="center" alignItems="center" justifyContent="space-between">
|
||||
<Grid2 size={5}>
|
||||
<Typography>
|
||||
{getFriendlyName(compData.type)}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Cable ID: {compData.cableId}
|
||||
</Typography>
|
||||
<Typography>
|
||||
Nodes: {compData.nodeCount}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2 size={5}>
|
||||
{subtypeOptions.length < 2
|
||||
?
|
||||
<Typography>
|
||||
{subtypeVal}
|
||||
</Typography>
|
||||
:
|
||||
subtypeSelector()}
|
||||
</Grid2>
|
||||
<Grid2 size={2}>
|
||||
{selectCompCheckbox()}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isMobile ? mobileDisplay() : desktopDisplay()}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
57
src/device/autoDetect/OneWire/ScannedOneWirePort.tsx
Normal file
57
src/device/autoDetect/OneWire/ScannedOneWirePort.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Box, MenuItem, TextField, Typography } 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"
|
||||
import { Component } from "models"
|
||||
|
||||
interface Props {
|
||||
scan: pond.DetectOneWire
|
||||
componentSelectionCallback: (newComponents: Component[], checked: boolean) => void
|
||||
}
|
||||
|
||||
export default function ScannedOneWirePort(props: Props){
|
||||
const {scan, componentSelectionCallback} = props
|
||||
const [portComponents, setPortComponents] = useState<quack.OneWireComponentData[]>([])
|
||||
|
||||
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 (
|
||||
<React.Fragment>
|
||||
{portComponents.length > 0 ? portComponents.map((compData, i) => {
|
||||
return (
|
||||
<PortComponent key={i} compData={compData} componentSelect={(checked, component) => {
|
||||
componentSelected(checked, component)
|
||||
}}/>
|
||||
)
|
||||
}):
|
||||
<Box>
|
||||
<Typography>No Components Found</Typography>
|
||||
</Box>}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,55 +56,6 @@ export default function GateDevice(props: Props) {
|
|||
const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 });
|
||||
const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 });
|
||||
|
||||
// const StyledToggleButtonGroup = withStyles(theme => ({
|
||||
// grouped: {
|
||||
// margin: theme.spacing(-0.5),
|
||||
// border: "none",
|
||||
// padding: theme.spacing(1),
|
||||
// "&:not(:first-child):not(:last-child)": {
|
||||
// borderRadius: 24,
|
||||
// marginRight: theme.spacing(0.5),
|
||||
// marginLeft: theme.spacing(0.5)
|
||||
// },
|
||||
// "&:first-child": {
|
||||
// borderRadius: 24,
|
||||
// marginLeft: theme.spacing(0.25)
|
||||
// },
|
||||
// "&:last-child": {
|
||||
// borderRadius: 24,
|
||||
// marginRight: theme.spacing(0.25)
|
||||
// }
|
||||
// },
|
||||
// root: {
|
||||
// backgroundColor: darken(
|
||||
// theme.palette.background.paper,
|
||||
// getThemeType() === "light" ? 0.05 : 0.25
|
||||
// ),
|
||||
// borderRadius: 24,
|
||||
// content: "border-box"
|
||||
// }
|
||||
// }))(ToggleButtonGroup);
|
||||
|
||||
// const StyledToggle = withStyles({
|
||||
// root: {
|
||||
// backgroundColor: "transparent",
|
||||
// overflow: "visible",
|
||||
// content: "content-box",
|
||||
// "&$selected": {
|
||||
// backgroundColor: "gold",
|
||||
// color: "black",
|
||||
// borderRadius: 24,
|
||||
// fontWeight: "bold"
|
||||
// },
|
||||
// "&$selected:hover": {
|
||||
// backgroundColor: "rgb(255, 255, 0)",
|
||||
// color: "black",
|
||||
// borderRadius: 24
|
||||
// }
|
||||
// },
|
||||
// selected: {}
|
||||
// })(ToggleButton);
|
||||
|
||||
useEffect(() => {
|
||||
if (comprehensiveDevice.device) {
|
||||
setDevice(Device.any(comprehensiveDevice.device));
|
||||
|
|
@ -141,7 +92,7 @@ export default function GateDevice(props: Props) {
|
|||
c.status.measurement[0].values[0] &&
|
||||
c.status.measurement[0].values[0].values[1]
|
||||
) {
|
||||
setPCAFanOn(c.status.measurement[0].values[0].values[1] > 250); //apx 1 iwg
|
||||
setPCAFanOn(c.status.measurement[0].values[0].values[1] > 996); //apx 4 iwg
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
@ -338,11 +289,11 @@ export default function GateDevice(props: Props) {
|
|||
return (
|
||||
<Grid
|
||||
container
|
||||
direction={(isMobile) ? "column" : "row"}
|
||||
// direction={(isMobile) ? "column" : "row"}
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
//alignItems="center"
|
||||
>
|
||||
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}>
|
||||
<Grid width={"100%"} size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
|
|
@ -363,7 +314,7 @@ export default function GateDevice(props: Props) {
|
|||
]}
|
||||
/>
|
||||
</Box>
|
||||
<Card raised>
|
||||
<Card>
|
||||
<GateSVG
|
||||
finalTemp={
|
||||
gate.gateMutations[device.id().toString()] &&
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ export default function GateFlowGraph(props: Props) {
|
|||
const classes = useStyles();
|
||||
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
|
||||
const [eventsLoading, setEventsLoading] = useState(false);
|
||||
const eventThreshold = 5;
|
||||
const idleFlow = 2.44;
|
||||
//these two constants could be entered by the user at time of retrieval
|
||||
const eventThreshold = 5; //the threshold that if crossed from one measurement to the next during a flow event will end the current flow event and start a new one
|
||||
const idleFlow = 3.5; //the mass air flow that must be crossed in order to start a flow event, anything below this will not start an event but must go below this to end an event
|
||||
|
||||
useEffect(() => {
|
||||
if (loadingChartData) return;
|
||||
|
|
@ -102,7 +103,6 @@ export default function GateFlowGraph(props: Props) {
|
|||
timestamp: time.valueOf(),
|
||||
value: val.airFlow ?? 0
|
||||
};
|
||||
|
||||
data.push(newPoint);
|
||||
if (!recent || recent.timestamp < newPoint.timestamp) {
|
||||
recent = newPoint;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { avg } from "utils";
|
||||
import GateFlowGraph from "./GateFlowGraph";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
interface Props {
|
||||
gate: Gate;
|
||||
|
|
@ -245,7 +246,9 @@ export default function GateGraphs(props: Props) {
|
|||
|
||||
const graphHeader = (comp: Component) => {
|
||||
const componentIcon = GetComponentIcon(comp.settings.type, comp.settings.subtype, themeType);
|
||||
|
||||
console.log(comp)
|
||||
let measurement = cloneDeep(comp.status.measurement)
|
||||
let umArray = measurement.map(um => UnitMeasurement.create(um, user))
|
||||
return (
|
||||
<CardHeader
|
||||
avatar={
|
||||
|
|
@ -260,9 +263,7 @@ export default function GateGraphs(props: Props) {
|
|||
subheader={
|
||||
<UnitMeasurementSummary
|
||||
component={comp}
|
||||
reading={UnitMeasurement.convertLastMeasurement(
|
||||
comp.status.measurement.map(um => UnitMeasurement.create(um, user)) ?? []
|
||||
)}
|
||||
reading={UnitMeasurement.convertLastMeasurement(umArray)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export default function GateSVG(props: Props) {
|
|||
const deviceCheckIn = () => {
|
||||
return (
|
||||
<g key={"deviceCheckIn"}>
|
||||
<text x={95} y={12} fontSize={7} fontWeight={650} fill={"white"}>
|
||||
<text x={85} y={12} fontSize={7} fontWeight={650} fill={"white"}>
|
||||
Last Checked In: {moment(checkInTime).fromNow()}
|
||||
</text>
|
||||
</g>
|
||||
|
|
@ -342,7 +342,7 @@ export default function GateSVG(props: Props) {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box height={"100%"} maxWidth={isMobile ? 360 : 460} margin="auto">
|
||||
<Box height={"100%"} maxWidth={isMobile ? 400 : 460} margin="auto">
|
||||
<svg
|
||||
width={"100%"}
|
||||
height={"100%"}
|
||||
|
|
|
|||
|
|
@ -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>(pond.BinYardSettings.create());
|
||||
const [openMarkerSettings, setOpenMarkerSettings] = useState(false);
|
||||
|
|
@ -85,7 +87,12 @@ export default function BinYardDrawer(props: Props) {
|
|||
|
||||
const drawerBody = () => {
|
||||
//return <Bins insert yardFilter={yard.key} />;
|
||||
return <BinyardDisplay insert yardKey={yard.key} />;
|
||||
return (
|
||||
<BinyardDisplay
|
||||
insert
|
||||
yardKey={yard.key}
|
||||
binClicked={binClicked}/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1659,6 +1659,18 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
|
|||
<React.Fragment>
|
||||
<BinYardDrawer
|
||||
open={yardDrawer}
|
||||
binClicked={(bin) => {
|
||||
//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);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,21 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = 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.10",
|
||||
v2CellGreen: "N/A",
|
||||
v2WifiBlue: "2.1.10",
|
||||
v2CellBlue: "2.1.10",
|
||||
v2EthBlue: "2.1.10"
|
||||
}
|
||||
]
|
||||
]);
|
||||
export class Device {
|
||||
|
|
@ -166,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) {
|
||||
|
|
@ -173,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
89
src/objects/objectInteractions/Alerts.tsx
Normal file
89
src/objects/objectInteractions/Alerts.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { Add, ExpandMore } from "@mui/icons-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";
|
||||
import React, { useState } from "react";
|
||||
import NewObjectInteraction from "./NewObjectInteraction";
|
||||
import { Option } from "common/SearchSelect";
|
||||
|
||||
export interface Alert {
|
||||
conditions: pond.InteractionCondition[];
|
||||
sourceComponents: Component[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
alerts: Alert[]
|
||||
permissions: pond.Permission[]
|
||||
addNew?: () => void
|
||||
}
|
||||
|
||||
export default function Alerts(props: Props){
|
||||
const {alerts, addNew, permissions} = 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 (
|
||||
<Box>
|
||||
<Typography>{type + " " + comparison + " " + value + describer.GetUnit()}</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const alertAccordion = (alert: Alert) => {
|
||||
return (
|
||||
<Accordion style={{ width: "100%" }}>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Grid2 container direction="column">
|
||||
{alert.conditions.map((condition, i) => (
|
||||
<Grid2 key={i}>
|
||||
{readableCondition(condition)}
|
||||
</Grid2>
|
||||
))}
|
||||
</Grid2>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List>
|
||||
<ListSubheader>Reporting Components</ListSubheader>
|
||||
{alert.sourceComponents.map((comp, i) => (
|
||||
<ListItem key={i}>{comp.name()}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
|
||||
<Typography>
|
||||
Alerts
|
||||
</Typography>
|
||||
{addNew &&
|
||||
<Button
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||
onClick={()=>{addNew()}}
|
||||
variant="contained"
|
||||
color="primary">
|
||||
<Add /> Alert
|
||||
</Button>
|
||||
}
|
||||
</ListSubheader>
|
||||
{alerts.map((alert, i) => (
|
||||
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
152
src/objects/objectInteractions/Controls.tsx
Normal file
152
src/objects/objectInteractions/Controls.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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 {
|
||||
conditions: pond.InteractionCondition[];
|
||||
sourceComponents: Component[];
|
||||
result: pond.InteractionResult | null;
|
||||
sinkComponent: Component
|
||||
device: Device
|
||||
}
|
||||
|
||||
interface Props {
|
||||
devices: Device[]
|
||||
controls: Control[]
|
||||
permissions: pond.Permission[]
|
||||
addNew?: (device: Device) => void
|
||||
}
|
||||
|
||||
export default function Controls(props: Props){
|
||||
const { devices, controls, addNew, permissions } = props
|
||||
const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map())
|
||||
const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null)
|
||||
|
||||
|
||||
useEffect(()=>{
|
||||
let map: Map<number, Control[]> = new Map()
|
||||
controls.forEach(control => {
|
||||
let mappedControls = map.get(control.device.id())
|
||||
if(mappedControls){
|
||||
mappedControls.push(control)
|
||||
}else{
|
||||
map.set(control.device.id(), [control])
|
||||
}
|
||||
})
|
||||
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 (
|
||||
<Accordion sx={{width: "100%"}}>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box>
|
||||
<Typography>
|
||||
{resultText}:
|
||||
</Typography>
|
||||
<Typography>
|
||||
{readableConditions(control)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List>
|
||||
<ListSubheader>Controlling Components</ListSubheader>
|
||||
{control.sourceComponents.map((comp, i) => (
|
||||
<ListItem key={i}>{comp.name()}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
const deviceMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
open={openDeviceMenu}
|
||||
anchorEl={anchor}
|
||||
onClose={() => {
|
||||
setAnchor(null)
|
||||
setOpenDeviceMenu(false)
|
||||
}}>
|
||||
{devices.map(dev => (
|
||||
<MenuItem
|
||||
key={dev.id()}
|
||||
onClick={() => {
|
||||
setAnchor(null)
|
||||
setOpenDeviceMenu(false)
|
||||
if (addNew) addNew(dev)
|
||||
|
||||
}}>
|
||||
{dev.name()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{deviceMenu()}
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
|
||||
<Typography>
|
||||
Controls
|
||||
</Typography>
|
||||
{addNew &&
|
||||
<Button
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||
variant="contained"
|
||||
onClick={(e)=>{
|
||||
setOpenDeviceMenu(true)
|
||||
setAnchor(e.currentTarget)
|
||||
}}
|
||||
color="primary">
|
||||
<Add />Control
|
||||
</Button>
|
||||
}
|
||||
</ListSubheader>
|
||||
{devices.map(device => (
|
||||
<React.Fragment key={device.id()}>
|
||||
<Typography sx={{paddingY: 1, paddingX: 2}}>{device.name()}</Typography>
|
||||
<Divider />
|
||||
{deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => (
|
||||
<ListItem key={i}>{controlAccordion(control)}</ListItem>
|
||||
))
|
||||
:
|
||||
<Typography sx={{paddingY: 1, paddingX: 2}}>No Control Interactions For Device</Typography>
|
||||
}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
111
src/objects/objectInteractions/Notifications.tsx
Normal file
111
src/objects/objectInteractions/Notifications.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
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<pond.Notification[]>([]);
|
||||
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 (
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader>
|
||||
<Typography sx={{paddingY: 2}}>
|
||||
Notifications
|
||||
</Typography>
|
||||
</ListSubheader>
|
||||
{recentNotifications && (
|
||||
<React.Fragment>
|
||||
{recentNotifications.map((notification, i) => (
|
||||
<ListItem key={i} className={i % 2 === 0 ? classes.dark : classes.light}>
|
||||
<Typography style={{ fontWeight: 650 }}>
|
||||
{moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
|
||||
</Typography>{" "}
|
||||
:{" "}
|
||||
<Typography>
|
||||
{notification.settings?.title + " - " + notification.settings?.subtitle}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
))}
|
||||
{recentNotifications.length < totalNotifications && (
|
||||
<ListItem key={"button"}>
|
||||
<Button onClick={loadMoreNotifications} disabled={notificationsLoading}>
|
||||
Get More
|
||||
</Button>
|
||||
</ListItem>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
229
src/objects/objectInteractions/ObjectInteractions.tsx
Normal file
229
src/objects/objectInteractions/ObjectInteractions.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import { Box, Button, Card, Typography } from "@mui/material";
|
||||
import { useCallback, 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";
|
||||
import Notifications from "./Notifications";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import React from "react";
|
||||
import NewObjectInteraction from "./NewObjectInteraction";
|
||||
|
||||
interface Props {
|
||||
linkedComponents: Map<string, Component>; //the component key to the component object
|
||||
componentDevices: Map<string, number>;
|
||||
objectKey: string
|
||||
objectType: pond.ObjectType
|
||||
devices: Device[];
|
||||
permissions: pond.Permission[]
|
||||
}
|
||||
|
||||
export default function ObjectInteractions(props: 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<Alert[]>([]);
|
||||
//list of interactions relating to a controller
|
||||
const [controls, setControls] = useState<Control[]>([]);
|
||||
const interactionsAPI = useInteractionsAPI();
|
||||
const [openNewInteraction, setOpenNewInteraction] = useState(false)
|
||||
const [allSinks, setAllSinks] = useState<Component[]>([]);
|
||||
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>(undefined)
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const load = useCallback(()=>{
|
||||
const newInteractions: Interaction[] = [];
|
||||
const interactionSourceMap = new Map<string, string>();
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setAllSinks(sinkOptions);
|
||||
setAlerts(alerts);
|
||||
setControls(controls);
|
||||
}).catch(err => {
|
||||
console.error("Interaction fetch error:", err)
|
||||
})
|
||||
},[linkedComponents, interactionsAPI, componentDevices, as])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NewObjectInteraction
|
||||
open={openNewInteraction}
|
||||
onClose={(refresh) => {
|
||||
setOpenNewInteraction(false)
|
||||
if(refresh){
|
||||
load()
|
||||
}
|
||||
}}
|
||||
linkedComponents={linkedComponents}
|
||||
componentDevices={componentDevices}
|
||||
device={selectedDevice}/>
|
||||
<Card raised style={{ margin: 5 }}>
|
||||
<Box padding={1}>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
|
||||
<Controls
|
||||
controls={controls}
|
||||
permissions={permissions}
|
||||
devices={devices}
|
||||
addNew={(device) => {
|
||||
setSelectedDevice(device)
|
||||
setOpenNewInteraction(true)
|
||||
}}/>
|
||||
<Alerts
|
||||
alerts={alerts}
|
||||
permissions={permissions}
|
||||
addNew={() => {
|
||||
setSelectedDevice(undefined)
|
||||
setOpenNewInteraction(true)
|
||||
}}/>
|
||||
<Notifications objectKey={objectKey} objectType={objectType}/>
|
||||
</Box>
|
||||
</Card>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -69,6 +67,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;
|
||||
|
|
@ -196,6 +195,7 @@ export default function Bin(props: Props) {
|
|||
const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
|
||||
const [heaters, setHeaters] = useState<Controller[]>([]);
|
||||
const [fans, setFans] = useState<Controller[]>([]);
|
||||
const [activeDetails, setActiveDetails] = useState([0])
|
||||
const [detail, setDetail] = useState<
|
||||
"inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions"
|
||||
>("inventory");
|
||||
|
|
@ -806,7 +806,7 @@ export default function Bin(props: Props) {
|
|||
{overview()}
|
||||
{preferences && binComponents(preferences)}
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
|
||||
<Grid id="tour-conditions" size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
|
||||
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
|
||||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
|
||||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && (
|
||||
|
|
@ -825,82 +825,66 @@ export default function Bin(props: Props) {
|
|||
<BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
|
||||
<Box paddingBottom={2} display="flex">
|
||||
<Grid id="tour-details" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
|
||||
<Box id="tour-graph-tabs" paddingBottom={2} display="flex">
|
||||
<ButtonGroup
|
||||
toggle
|
||||
toggledButtons={activeDetails}
|
||||
buttons={[
|
||||
{
|
||||
title: "Inventory",
|
||||
function: () => 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])
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{/* <StyledToggleButtonGroup
|
||||
id="tour-graph-tabs"
|
||||
value={detail}
|
||||
exclusive
|
||||
size="small"
|
||||
aria-label="detail">
|
||||
<StyledToggle
|
||||
value={"inventory"}
|
||||
aria-label="Inventory Details"
|
||||
onClick={() => setDetail("inventory")}>
|
||||
Inventory
|
||||
</StyledToggle>
|
||||
<StyledToggle
|
||||
onClick={() => setDetail("sensors")}
|
||||
value={"sensors"}
|
||||
aria-label="Bin Sensor Graphs">
|
||||
Sensors
|
||||
</StyledToggle>
|
||||
<StyledToggle
|
||||
onClick={() => setDetail("analytics")}
|
||||
value={"analytics"}
|
||||
aria-label="Bin Analysis Graphs">
|
||||
Analysis
|
||||
</StyledToggle>
|
||||
<StyledToggle
|
||||
onClick={() => setDetail("alerts")}
|
||||
value={"alerts"}
|
||||
aria-label="Device Alerts">
|
||||
Alerts
|
||||
</StyledToggle>
|
||||
<StyledToggle
|
||||
onClick={() => setDetail("presets")}
|
||||
value={"presets"}
|
||||
aria-label="Bin Mode Presets">
|
||||
Presets
|
||||
</StyledToggle>
|
||||
</StyledToggleButtonGroup> */}
|
||||
</Box>
|
||||
{detail === "alerts" && (
|
||||
<Box>
|
||||
<ObjectAlerts
|
||||
<ObjectInteractions
|
||||
linkedComponents={components}
|
||||
componentDevices={componentDevices}
|
||||
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
||||
devices={devices}
|
||||
objectKey={bin.key()}
|
||||
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
||||
permissions={permissions}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
|
@ -916,7 +900,13 @@ export default function Bin(props: Props) {
|
|||
/>
|
||||
</Box>
|
||||
)}
|
||||
{detail === "transactions" &&
|
||||
<Box>
|
||||
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
|
||||
</Box>
|
||||
}
|
||||
{(detail === "inventory" || detail === "sensors" || detail === "analytics") && (
|
||||
<Box>
|
||||
<BinGraphs
|
||||
bin={bin}
|
||||
binLoading={binLoading}
|
||||
|
|
@ -929,10 +919,8 @@ export default function Bin(props: Props) {
|
|||
compMap={components}
|
||||
compositionNameMap={compositionNameMap}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{detail === "transactions" &&
|
||||
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
|
||||
}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Card>
|
||||
|
|
@ -1033,11 +1021,13 @@ export default function Bin(props: Props) {
|
|||
</TabPanelMine>
|
||||
<TabPanelMine value={mobileTab} index={4}>
|
||||
<Box>
|
||||
<ObjectAlerts
|
||||
<ObjectInteractions
|
||||
linkedComponents={components}
|
||||
componentDevices={componentDevices}
|
||||
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
||||
devices={devices}
|
||||
objectKey={bin.key()}
|
||||
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
|
||||
permissions={permissions}
|
||||
/>
|
||||
</Box>
|
||||
</TabPanelMine>
|
||||
|
|
@ -1175,7 +1165,11 @@ export default function Bin(props: Props) {
|
|||
</Box>
|
||||
{objectTeams()}
|
||||
{deviceMenu()}
|
||||
<BinTour />
|
||||
<BinTour
|
||||
setDetailTabState={(detail, buttonIndex) => {
|
||||
setDetail(detail)
|
||||
setActiveDetails([buttonIndex])
|
||||
}}/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ export default function Bins(props: Props) {
|
|||
const [cardValDisplay, setCardValDisplay] = useState<"high" | "low" | "average">("high");
|
||||
|
||||
const [teamsDialog, setTeamsDialog] = useState(false)
|
||||
const snackbar = useSnackbar()
|
||||
|
||||
// const [scrollTranslations, setScrollTranslations] = useState({
|
||||
// bins: 0,
|
||||
|
|
@ -265,6 +266,8 @@ export default function Bins(props: Props) {
|
|||
// console.log("should?")
|
||||
setTeamsDialog(true)
|
||||
}
|
||||
}).catch(err => {
|
||||
snackbar.error("error listing teams: "+err)
|
||||
})
|
||||
}
|
||||
}, [doneLoadingPage, grainBins, grainBags])
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -158,9 +158,9 @@ export default function DevicePage() {
|
|||
if (length > 0) {
|
||||
warningString = warningString + " through " + getContextTypes()[length - 1];
|
||||
}
|
||||
snackbar.warning(warningString);
|
||||
snackbar.warning("page data warning: "+warningString);
|
||||
} else {
|
||||
snackbar.error(err);
|
||||
snackbar.error("page data error: "+err);
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
|
|
@ -169,12 +169,12 @@ export default function DevicePage() {
|
|||
const loadPortScan = () => {
|
||||
deviceAPI.listFoundComponents(parseInt(deviceID))
|
||||
.then(resp => {
|
||||
if (resp.data.foundComponents?.i2c){
|
||||
setScannedAddresses(resp.data.foundComponents ?? undefined)
|
||||
if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
|
||||
setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
console.log("load port scan: "+err)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +411,16 @@ export default function DevicePage() {
|
|||
/>
|
||||
</Grid>
|
||||
{/* add grid card here for I2C detected components */}
|
||||
|
||||
{(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<DeviceScannedComponents
|
||||
scannedComponents={scannedAddresses}
|
||||
device={device}
|
||||
availablePositions={availablePositions}
|
||||
availableOffsets={availableOffsets}
|
||||
refreshCallback={loadDevice}/>
|
||||
</Grid>
|
||||
}
|
||||
{diagnosticComponents.map(comp => (
|
||||
<Grid size={{ xs: 12 }} key={comp.key()}>
|
||||
<ComponentDiagnostics
|
||||
|
|
@ -432,8 +441,8 @@ export default function DevicePage() {
|
|||
refreshCallback={loadDevice}
|
||||
/>
|
||||
</Grid>
|
||||
{scannedAddresses &&
|
||||
<Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}>
|
||||
{(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
|
||||
<Grid size={{ xs: 6, sm: 6, lg: 8, xl: 8 }}>
|
||||
<DeviceScannedComponents
|
||||
scannedComponents={scannedAddresses}
|
||||
device={device}
|
||||
|
|
|
|||
|
|
@ -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<User>(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 (
|
||||
<Grid2 container>
|
||||
<Grid2 size={{ xs: 12 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
id="pressureUnit"
|
||||
name="pressureUnit"
|
||||
label="Pressure Unit"
|
||||
value={pressureUnit ? pressureUnit : pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER}
|
||||
onChange={event => changePressureUnit(event.target.value)}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem value={pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS}>
|
||||
Kilopascals (kPa)
|
||||
</MenuItem>
|
||||
<MenuItem value={pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER}>
|
||||
Inches of Water (iwg)
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
id="temperatureUnit"
|
||||
name="temperatureUnit"
|
||||
label="Temperature Unit"
|
||||
value={
|
||||
temperatureUnit ? temperatureUnit : pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS
|
||||
}
|
||||
onChange={event => changeTemperatureUnit(event.target.value)}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem value={pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS}>Celsius (°C)</MenuItem>
|
||||
<MenuItem value={pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT}>
|
||||
Fahrenheit (°F)
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
id="distanceUnit"
|
||||
name="distanceUnit"
|
||||
label="Distance Unit"
|
||||
value={distanceUnit ? distanceUnit : pond.DistanceUnit.DISTANCE_UNIT_METERS}
|
||||
// onChange={event => changeDistanceUnit(event.target.value)}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_METERS}>Meters (m)</MenuItem>
|
||||
<MenuItem value={pond.DistanceUnit.DISTANCE_UNIT_FEET}>Feet (ft)</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
{IsAdaptiveAgriculture() && (
|
||||
<Grid2 size={{ xs: 12 }}>
|
||||
<TextField
|
||||
select
|
||||
fullWidth
|
||||
id="grainUnit"
|
||||
name="grainUnit"
|
||||
label="Grain Unit"
|
||||
value={grainUnit ? grainUnit : pond.GrainUnit.GRAIN_UNIT_BUSHELS}
|
||||
// onChange={event => changeGrainUnit(event.target.value)}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_BUSHELS}>Bushels (bu)</MenuItem>
|
||||
<MenuItem value={pond.GrainUnit.GRAIN_UNIT_WEIGHT}>Tonnes (mT)</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
)}
|
||||
</Grid2>
|
||||
);
|
||||
};
|
||||
|
||||
const content = () => {
|
||||
return (
|
||||
<DialogContent sx={{ margin: "auto", textAlign: "center" }}>
|
||||
|
|
@ -104,12 +225,17 @@ export default function SignupCallback () {
|
|||
content: (
|
||||
<>
|
||||
<Typography variant="body2" >
|
||||
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.
|
||||
</Typography>
|
||||
<br/>
|
||||
<Typography variant="body2" >
|
||||
If you are part of a team, you can select it here as well.
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography variant="body2" >
|
||||
Set your unit preferences here now, they can be changed later from this menu:
|
||||
</Typography>
|
||||
{unitPreferences()}
|
||||
</>
|
||||
),
|
||||
target: "#tour-user-menu",
|
||||
|
|
@ -150,7 +276,59 @@ export default function SignupCallback () {
|
|||
target: "#tour-dashboard",
|
||||
placement: "right"
|
||||
})
|
||||
if (IsAdaptiveAgriculture()) steps.push({
|
||||
steps.push({
|
||||
title: "Tasks",
|
||||
content: (
|
||||
<>
|
||||
<Typography variant="body2" >
|
||||
You can view and create tasks here and assign a start and end time for them.
|
||||
</Typography>
|
||||
<br/>
|
||||
<Typography>
|
||||
If you are viewing as a team you will see the teams tasks, you can even assign tasks to team members.
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
target: "#tour-tasks",
|
||||
placement: "right"
|
||||
})
|
||||
//tasks step
|
||||
if (IsAdaptiveAgriculture()) {
|
||||
steps.push({
|
||||
title: "Visual Farm",
|
||||
content: (
|
||||
<>
|
||||
<Typography variant="body2" >
|
||||
You can view your Visual Farm here, it will allow tou to draw fields and plot bins an a map.
|
||||
</Typography>
|
||||
<br/>
|
||||
<Typography>
|
||||
If you are viewing as a team, your team's fields and bins will be displayed here instead.
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
target: "#tour-visual-farm",
|
||||
placement: "right",
|
||||
disableBeacon: true,
|
||||
})
|
||||
steps.push({
|
||||
title: "Fields",
|
||||
content: (
|
||||
<>
|
||||
<Typography variant="body2" >
|
||||
You can view your fields you have drawn on the map here.
|
||||
</Typography>
|
||||
<br/>
|
||||
<Typography>
|
||||
If you are viewing as a team, your team's fields will be displayed here instead.
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
target: "#tour-field-list",
|
||||
placement: "right",
|
||||
disableBeacon: true,
|
||||
})
|
||||
steps.push({
|
||||
title: "Bins",
|
||||
content: (
|
||||
<>
|
||||
|
|
@ -167,18 +345,35 @@ export default function SignupCallback () {
|
|||
placement: "right",
|
||||
disableBeacon: true,
|
||||
})
|
||||
steps.push({
|
||||
title: "Contracts",
|
||||
content: (
|
||||
<>
|
||||
<Typography variant="body2" >
|
||||
You can view and track any contracts you have created here.
|
||||
</Typography>
|
||||
<br/>
|
||||
<Typography>
|
||||
If you are viewing as a team, your team's contracts will be displayed here instead.
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
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 = () => {
|
||||
|
|
|
|||
|
|
@ -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<number, number[]>
|
||||
isChainable?: boolean
|
||||
}
|
||||
|
||||
export interface Summary {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export function CapacitorCable(subtype: number = 0): ComponentTypeExtension {
|
|||
isSource: true,
|
||||
isArray: true,
|
||||
isCalibratable: false,
|
||||
isChainable: true,
|
||||
addressTypes: addressTypes,
|
||||
interactionResultTypes: [],
|
||||
states: [],
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface IDeviceAPIContext {
|
|||
) => Promise<AxiosResponse<pond.GetDevicePageDataResponse>>;
|
||||
getMulti: (ids: number[] | string[], otherTeam?: string) => Promise<AxiosResponse<pond.GetMultiDeviceResponse>>;
|
||||
detectI2C: (id: number, otherTeam?: string) => Promise<AxiosResponse<pond.DetectI2CResponse>>;
|
||||
detectOneWire: (id: number, port: number, otherTeam?: string) => Promise<AxiosResponse<pond.DetectOneWireResponse>>
|
||||
listFoundComponents: (id: number, otherTeam?: string) => Promise<AxiosResponse<pond.ListFoundComponentsResponse>>
|
||||
getGeoJson: (id: number | string, demo?: boolean, otherTeam?: string) => Promise<any>;
|
||||
getMultiGeoJson: (ids: number[] | string[], otherTeam?: string) => Promise<any>;
|
||||
|
|
@ -148,7 +149,8 @@ export interface IDeviceAPIContext {
|
|||
keys?: string[],
|
||||
types?: string[]
|
||||
) => Promise<any>;
|
||||
removeFoundComponents: (id: number, key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveFoundComponentsResponse>>;
|
||||
removeAllFoundComponents: (id: number,otherTeam?: string) => Promise<AxiosResponse<pond.RemoveAllFoundComponentsResponse>>;
|
||||
removeFoundComponents: (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveFoundComponentsResponse>>;
|
||||
}
|
||||
|
||||
export const DeviceAPIContext = createContext<IDeviceAPIContext>({} as IDeviceAPIContext);
|
||||
|
|
@ -962,6 +964,19 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
|
|||
})
|
||||
}
|
||||
|
||||
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<AxiosResponse<pond.DetectOneWireResponse>>((resolve, reject) => {
|
||||
put<pond.DetectOneWireResponse>(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
|
||||
|
|
@ -975,10 +990,10 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
|
|||
})
|
||||
}
|
||||
|
||||
const removeFoundComponents = (id: number, key: string, otherTeam?: string) => {
|
||||
let url = "/devices/" + id + "/removeFoundComponents/" + key;
|
||||
const removeFoundComponents = (id: number, key: string, type: pond.ObjectType, otherTeam?: string) => {
|
||||
let url = "/devices/" + id + "/removeFoundComponents/" + key + "?scanType=" + type;
|
||||
const view = otherTeam ? otherTeam : as
|
||||
if(view) url = url + "?as=" + view
|
||||
if(view) url = url + "&as=" + view
|
||||
return new Promise<AxiosResponse<pond.RemoveFoundComponentsResponse>>((resolve, reject) => {
|
||||
del<pond.RemoveFoundComponentsResponse>(pondURL(url)).then(resp => {
|
||||
return resolve(resp)
|
||||
|
|
@ -988,6 +1003,19 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
|
|||
})
|
||||
}
|
||||
|
||||
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<AxiosResponse<pond.RemoveAllFoundComponentsResponse>>((resolve, reject) => {
|
||||
del<pond.RemoveAllFoundComponentsResponse>(pondURL(url)).then(resp => {
|
||||
return resolve(resp)
|
||||
}).catch(err => {
|
||||
return reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<DeviceAPIContext.Provider
|
||||
value={{
|
||||
|
|
@ -1035,8 +1063,10 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
|
|||
updateComponentPreferences,
|
||||
listDeviceComponentPreferences,
|
||||
detectI2C,
|
||||
detectOneWire,
|
||||
listFoundComponents,
|
||||
removeFoundComponents
|
||||
removeFoundComponents,
|
||||
removeAllFoundComponents
|
||||
}}>
|
||||
{children}
|
||||
</DeviceAPIContext.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue