406 lines
No EOL
15 KiB
TypeScript
406 lines
No EOL
15 KiB
TypeScript
import { AppBar, Box, Button, Card, CardActionArea, Checkbox, DialogActions, DialogContent, DialogTitle, FormControlLabel, Grid2, IconButton, Typography } from "@mui/material";
|
|
import { green } from "@mui/material/colors";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { CableData } from "bin/3dView/Data/BuildCableData";
|
|
import { NodeData } from "bin/3dView/Data/BuildNodeData";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import HumidityIcon from "component/HumidityIcon";
|
|
import TemperatureIcon from "component/TemperatureIcon";
|
|
import { useComponentAPI, useMobile, useSnackbar } from "hooks";
|
|
import InteractionsOverview from "interactions/InteractionsOverview";
|
|
import { Bin, Component, Device, Interaction } from "models";
|
|
import moment from "moment";
|
|
import AddIcon from "@mui/icons-material/AddCircle";
|
|
import GraphIcon from "products/CommonIcons/graphIcon";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { useBinAPI, useGlobalState, useInteractionsAPI } from "providers";
|
|
import React from "react";
|
|
import { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { canWrite } from "pbHelpers/Permission";
|
|
import InteractionSettings from "interactions/InteractionSettings";
|
|
|
|
const useStyles = makeStyles(() => {
|
|
return ({
|
|
dialog: {
|
|
maxWidth: 350
|
|
},
|
|
appBar: {
|
|
position: "static",
|
|
borderRadius: 30
|
|
},
|
|
readingCard: {
|
|
padding: 10
|
|
},
|
|
greenButton: {
|
|
color: green["600"],
|
|
padding: 0
|
|
}
|
|
});
|
|
});
|
|
|
|
interface Props {
|
|
open: boolean
|
|
onClose: () => void
|
|
node: NodeData
|
|
cable: CableData
|
|
device: Device
|
|
bin: Bin
|
|
/**
|
|
* the possible options for controllers when creating new interactions, these controllers must be on the same device as the cable for interactions to work
|
|
*/
|
|
filteredComponents?: Component[]
|
|
componentMap: Map<string, Component>
|
|
permissions: pond.Permission[]
|
|
/**
|
|
* callback function for updating the cable with a new top node or excluded node
|
|
* @returns void
|
|
*/
|
|
updateComponentCallback?: () => void
|
|
}
|
|
|
|
export default function NodeControls(props: Props){
|
|
const {open, onClose, node, cable, device, bin, filteredComponents, permissions, updateComponentCallback, componentMap} = props
|
|
const classes = useStyles()
|
|
const [isTopNode, setIsTopNode] = useState(false)
|
|
const [isExcluded, setIsExcluded] = useState(false)
|
|
const [cableComponent, setCableComponent] = useState<Component>(Component.create())
|
|
const [cableInteractions, setCableInteractions] = useState<Interaction[]>([])
|
|
const isMobile = useMobile()
|
|
const navigate = useNavigate()
|
|
const [{as, user}] = useGlobalState()
|
|
const interactionAPI = useInteractionsAPI()
|
|
const binAPI = useBinAPI()
|
|
const componentAPI = useComponentAPI()
|
|
const {openSnack} = useSnackbar()
|
|
const [newInteraction, setNewInteraction] = useState(false);
|
|
|
|
//will need to list the interactions for the cable
|
|
useEffect(() => {
|
|
let component = componentMap.get(cable.grainCable.key) ?? Component.create()
|
|
setCableComponent(component)
|
|
interactionAPI.listInteractionsByComponent(device.id(), cableComponent.location(), undefined, as).then(resp => {
|
|
setCableInteractions(resp);
|
|
});
|
|
},[cable, device, interactionAPI])
|
|
|
|
//determine if the node is excluded/the top node
|
|
useEffect(()=>{
|
|
if(node.topNode !== undefined){
|
|
setIsTopNode(node.topNode)
|
|
}
|
|
if(cable.grainCable.excludedNodes.includes(node.nodeIndex)){
|
|
setIsExcluded(true)
|
|
}else(
|
|
setIsExcluded(false)
|
|
)
|
|
},[node, cable])
|
|
|
|
//navigation function to go to the component page
|
|
const goToComponent = () => {
|
|
navigate("/devices/" + device.id() + "/components/" + cable.grainCable.key);
|
|
}
|
|
|
|
const close = ()=>{
|
|
onClose()
|
|
}
|
|
|
|
const setEMC = () => {
|
|
let settings = cableComponent.settings;
|
|
//make sure the mutation is not there first
|
|
if(!settings.defaultMutations.includes(pond.Mutator.MUTATOR_EMC)){
|
|
settings.defaultMutations.push(pond.Mutator.MUTATOR_EMC)
|
|
}
|
|
settings.grainType = bin.grain();
|
|
settings.customGrain = bin.customGrain();
|
|
componentAPI
|
|
.update(device.id(), settings, [bin.key()], ["bin"], as)
|
|
.then(resp => {
|
|
openSnack("EMC set on cable " + cableComponent.name());
|
|
})
|
|
.catch(err => {});
|
|
}
|
|
|
|
const submit = () => {
|
|
//should update the component and then update the bin prefs if successful
|
|
componentAPI.update(device.id(), cableComponent.settings).then(resp => {
|
|
let pref = pond.BinComponentPreferences.create({
|
|
type: pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE,
|
|
node: cableComponent.settings.grainFilledTo
|
|
});
|
|
//update the bins preferences to have the new top node for the cable
|
|
binAPI
|
|
.updateComponentPreferences(bin.key(), cableComponent.key(), pref, as)
|
|
.then(resp => {
|
|
openSnack("Changes will be reflected once the bins status updates")
|
|
if(updateComponentCallback){
|
|
updateComponentCallback()
|
|
}
|
|
})
|
|
.catch(err => {});
|
|
}).catch(err => {
|
|
|
|
})
|
|
close()
|
|
}
|
|
|
|
const updateTopNode = (checked: boolean) => {
|
|
if (isTopNode && !checked) {
|
|
cableComponent.settings.grainFilledTo = 0;
|
|
cableInteractions.forEach(interaction => {
|
|
interaction.settings.subtype = 0;
|
|
interaction.settings.nodeOne = 0;
|
|
interaction.settings.nodeTwo = 0;
|
|
});
|
|
} else if (checked) {
|
|
cableComponent.settings.grainFilledTo = node.nodeIndex+1;
|
|
cableInteractions.forEach(interaction => {
|
|
interaction.settings.subtype = Interaction.upToSubtype(node.nodeIndex+1);
|
|
interaction.settings.nodeOne = node.nodeIndex+1;
|
|
});
|
|
}
|
|
setIsTopNode(checked);
|
|
}
|
|
|
|
const updateNodeExclusion = (checked: boolean) => {
|
|
if (checked) {
|
|
cableComponent.settings.excludedNodes.push(node.nodeIndex)
|
|
} else {
|
|
cableComponent.settings.excludedNodes.splice(cableComponent.settings.excludedNodes.indexOf(node.nodeIndex), 1)
|
|
}
|
|
setIsExcluded(checked)
|
|
}
|
|
|
|
const displayTemp = () => {
|
|
let isF = user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
|
|
let tempFinal = node.celcius ?? 0;
|
|
if (isF) {
|
|
tempFinal = (tempFinal * 1.8 + 32);
|
|
}
|
|
return tempFinal.toFixed(2) + (isF ? " F" : "C");
|
|
};
|
|
|
|
const latestReading = () => {
|
|
return (
|
|
<Box>
|
|
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Latest Reading</Typography>
|
|
<Typography style={{ fontSize: 10 }}>{moment(cable.grainCable.lastRead).fromNow()}</Typography>
|
|
<Grid2 container direction="row" spacing={2} style={{ padding: 10 }}>
|
|
<Grid2 size={6}>
|
|
<Card className={classes.readingCard}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<TemperatureIcon />
|
|
<Typography style={{ fontWeight: 650 }}>
|
|
{displayTemp()}
|
|
</Typography>
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
Temperature
|
|
</Typography>
|
|
</Card>
|
|
</Grid2>
|
|
<Grid2 size={6}>
|
|
<Card className={classes.readingCard}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<HumidityIcon />
|
|
<Typography style={{ fontWeight: 650 }}>
|
|
{node.humidity} %
|
|
</Typography>
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
Humidity
|
|
</Typography>
|
|
</Card>
|
|
</Grid2>
|
|
<Grid2 size={6}>
|
|
{/* only show the emc if the bin grain type and the cable grain type match */}
|
|
{node.moisture ? (
|
|
<Card className={classes.readingCard}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<HumidityIcon />
|
|
<Typography style={{ fontWeight: 650 }}>
|
|
{node.moisture} %
|
|
</Typography>
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
Grain EMC
|
|
</Typography>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardActionArea
|
|
className={classes.readingCard}
|
|
onClick={() => {
|
|
setEMC();
|
|
}}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<HumidityIcon />
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
Set EMC
|
|
</Typography>
|
|
</CardActionArea>
|
|
</Card>
|
|
)}
|
|
</Grid2>
|
|
<Grid2 size={6}>
|
|
<Card>
|
|
<CardActionArea className={classes.readingCard} onClick={() => goToComponent()}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<GraphIcon />
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
See Graphs
|
|
</Typography>
|
|
</CardActionArea>
|
|
</Card>
|
|
</Grid2>
|
|
</Grid2>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const nodeControl = () => {
|
|
return (
|
|
<Box style={{ marginTop: 10 }}>
|
|
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Node Control</Typography>
|
|
<FormControlLabel
|
|
style={{ marginTop: 10 }}
|
|
control={
|
|
<Checkbox
|
|
checked={isExcluded}
|
|
value={isExcluded}
|
|
onChange={e => {
|
|
updateNodeExclusion(e.target.checked);
|
|
}}
|
|
name="excludedNode"
|
|
/>
|
|
}
|
|
label={
|
|
<React.Fragment>
|
|
<Typography style={{ fontSize: 15, fontWeight: 650 }}>
|
|
Exclude Node
|
|
</Typography>
|
|
<Typography style={{ fontSize: 15 }}>
|
|
If checked this will set the node to be disabled and will not be used for any calculations done for the bin
|
|
</Typography>
|
|
</React.Fragment>
|
|
}
|
|
/>
|
|
<FormControlLabel
|
|
style={{ marginTop: 10 }}
|
|
control={
|
|
<Checkbox
|
|
checked={isTopNode}
|
|
value={isTopNode}
|
|
disabled={isExcluded}
|
|
onChange={e => {
|
|
updateTopNode(e.target.checked);
|
|
}}
|
|
name="topNode"
|
|
/>
|
|
}
|
|
label={
|
|
<React.Fragment>
|
|
<Typography style={{ fontSize: 15, fontWeight: 650 }}>
|
|
Set as top node in grain
|
|
</Typography>
|
|
<Typography style={{ fontSize: 15 }}>
|
|
If checked this will set interactions to ignore all nodes above it. Interactions can
|
|
still be adjusted manually to use all nodes
|
|
</Typography>
|
|
</React.Fragment>
|
|
}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const interactionDisplay = () => {
|
|
return (
|
|
<Box marginTop={2}>
|
|
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Interactions</Typography>
|
|
<Box marginTop={1} marginBottom={1}>
|
|
<InteractionsOverview
|
|
component={cableComponent}
|
|
components={filteredComponents ?? []}
|
|
device={device}
|
|
interactions={cableInteractions}
|
|
permissions={permissions}
|
|
refreshCallback={() => {
|
|
interactionAPI
|
|
.listInteractionsByComponent(device.id(), cableComponent.location(), undefined, as)
|
|
.then(resp => {
|
|
setCableInteractions(resp);
|
|
});
|
|
}}
|
|
/>
|
|
</Box>
|
|
<Box display="flex" justifyContent="center">
|
|
<IconButton
|
|
color="primary"
|
|
disabled={!canWrite(permissions)}
|
|
aria-label="Add Interaction"
|
|
onClick={() => {
|
|
setNewInteraction(true);
|
|
}}
|
|
className={classes.greenButton}>
|
|
<AddIcon />
|
|
</IconButton>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<ResponsiveDialog open={open} onClose={close} fullWidth={isMobile}>
|
|
<DialogTitle style={{ padding: 10 }}>
|
|
<Box display="flex" justifyContent="center" marginBottom={2}>
|
|
<Typography style={{ fontSize: 30, fontWeight: 650 }}>{cable.grainCable.name}</Typography>
|
|
</Box>
|
|
<AppBar
|
|
className={classes.appBar}
|
|
color="secondary">
|
|
<Box>
|
|
<Typography align="center" variant="h5">
|
|
Node {node.nodeIndex+1}
|
|
</Typography>
|
|
</Box>
|
|
</AppBar>
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
{latestReading()}
|
|
{nodeControl()}
|
|
{interactionDisplay()}
|
|
{/*
|
|
*/}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={close} color="primary">
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={submit} color="primary">
|
|
Submit
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
<InteractionSettings
|
|
isDialogOpen={newInteraction}
|
|
initialComponent={cableComponent}
|
|
mode="add"
|
|
device={device}
|
|
components={filteredComponents ?? []}
|
|
closeDialogCallback={() => {
|
|
setNewInteraction(false);
|
|
}}
|
|
refreshCallback={() => {
|
|
interactionAPI.listInteractionsByComponent(device.id(), cableComponent.location(), undefined, as).then(resp => {
|
|
setCableInteractions(resp);
|
|
});
|
|
}}
|
|
canEdit={canWrite(permissions)}
|
|
/>
|
|
</React.Fragment>
|
|
)
|
|
} |