frontend/src/bin/binSummary/components/nodeControls.tsx

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>
)
}