507 lines
16 KiB
TypeScript
507 lines
16 KiB
TypeScript
import {
|
|
AppBar,
|
|
Box,
|
|
Button,
|
|
Card,
|
|
CardActionArea,
|
|
Checkbox,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
FormControlLabel,
|
|
Grid,
|
|
IconButton,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { ArrowBackIos, ArrowForwardIos } from "@mui/icons-material";
|
|
import HumidityIcon from "component/HumidityIcon";
|
|
import TemperatureIcon from "component/TemperatureIcon";
|
|
import { useMobile, useSnackbar } from "hooks";
|
|
import InteractionSettings from "interactions/InteractionSettings";
|
|
import InteractionsOverview from "interactions/InteractionsOverview";
|
|
import { clone } from "lodash";
|
|
import { Component, Device } from "models";
|
|
import { GrainCable } from "models/GrainCable";
|
|
import { Interaction } from "models/Interaction";
|
|
import moment from "moment";
|
|
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
|
import { canWrite } from "pbHelpers/Permission";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { quack } from "protobuf-ts/quack";
|
|
import { useBinAPI, useComponentAPI, useGlobalState, useInteractionsAPI } from "providers";
|
|
import AddIcon from "@mui/icons-material/AddCircle";
|
|
import React, { useEffect, useState } from "react";
|
|
// import { useHistory } from "react-router";
|
|
import { green } from "@mui/material/colors";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import GraphIcon from "products/CommonIcons/graphIcon";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
device: Device;
|
|
cable: GrainCable;
|
|
binKey: string;
|
|
selectedNode: number;
|
|
close: () => void;
|
|
grain: pond.Grain;
|
|
interactionComponents: Component[];
|
|
permissions: pond.Permission[];
|
|
updateComponentCallback: (componentKey: string) => void;
|
|
}
|
|
|
|
const useStyles = makeStyles(() => {
|
|
return ({
|
|
dialog: {
|
|
maxWidth: 350
|
|
},
|
|
appBar: {
|
|
position: "static",
|
|
borderRadius: 30
|
|
},
|
|
readingCard: {
|
|
padding: 10
|
|
},
|
|
greenButton: {
|
|
color: green["600"],
|
|
padding: 0
|
|
}
|
|
});
|
|
});
|
|
|
|
export default function GrainNodeInteractions(props: Props) {
|
|
const {
|
|
open,
|
|
selectedNode,
|
|
device,
|
|
cable,
|
|
close,
|
|
binKey,
|
|
grain,
|
|
interactionComponents,
|
|
permissions,
|
|
updateComponentCallback
|
|
} = props;
|
|
const [nodeTemp, setNodeTemp] = useState<number>();
|
|
const [nodeHum, setNodeHum] = useState<number>();
|
|
const [nodeMoist, setNodeMoist] = useState<number | undefined>();
|
|
const [{ user, as }] = useGlobalState();
|
|
const tempDescriber = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE,
|
|
quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE,
|
|
undefined,
|
|
undefined,
|
|
user);
|
|
const humDescriber = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_PERCENT,
|
|
quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE,
|
|
undefined,
|
|
undefined,
|
|
user);
|
|
const moistureDescriber = describeMeasurement(
|
|
quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC,
|
|
quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE,
|
|
undefined,
|
|
undefined,
|
|
user);
|
|
const [topNode, setTopNode] = useState(false);
|
|
const [excluded, setExcluded] = useState(false);
|
|
const componentAPI = useComponentAPI();
|
|
const interactionAPI = useInteractionsAPI();
|
|
const binAPI = useBinAPI();
|
|
const [displayNode, setDisplayNode] = useState(0);
|
|
const classes = useStyles();
|
|
const isMobile = useMobile();
|
|
// const history = useHistory();
|
|
const navigate = useNavigate()
|
|
const [cableInteractions, setCableInteractions] = useState<Interaction[]>([]);
|
|
const { openSnack } = useSnackbar();
|
|
const [newInteraction, setNewInteraction] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setDisplayNode(selectedNode);
|
|
}, [selectedNode]);
|
|
|
|
useEffect(() => {
|
|
interactionAPI.listInteractionsByComponent(device.id(), cable.location(), undefined, as).then(resp => {
|
|
setCableInteractions(resp);
|
|
});
|
|
}, [cable, device, interactionAPI, as]);
|
|
|
|
useEffect(() => {
|
|
//do the reversing to make the selected node match the array in the
|
|
let revTemps = clone(cable.temperatures);
|
|
revTemps.reverse();
|
|
setNodeTemp(revTemps[displayNode - 1]);
|
|
let revHums = clone(cable.humidities);
|
|
revHums.reverse();
|
|
setNodeHum(revHums[displayNode - 1]);
|
|
let revMoist = clone(cable.grainMoistures);
|
|
revMoist.reverse();
|
|
setNodeMoist(revMoist[displayNode - 1]);
|
|
|
|
//determine whether the node being looked at is the active "top fill" node
|
|
if (displayNode === cable.settings.grainFilledTo) {
|
|
setTopNode(true);
|
|
} else {
|
|
setTopNode(false);
|
|
}
|
|
|
|
//determine if the node is excluded
|
|
if (cable.excludedNodes.includes(displayNode - 1)){
|
|
setExcluded(true)
|
|
}else{
|
|
setExcluded(false)
|
|
}
|
|
}, [cable, displayNode]);
|
|
|
|
const goToComponent = () => {
|
|
navigate("/devices/" + device.id() + "/components/" + cable.key());
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
setDisplayNode(selectedNode);
|
|
close();
|
|
};
|
|
|
|
const setEMC = () => {
|
|
let settings = cable.settings;
|
|
settings.defaultMutations = [pond.Mutator.MUTATOR_EMC];
|
|
settings.grainType = grain;
|
|
componentAPI
|
|
.update(device.id(), settings, [binKey], ["bin"], as)
|
|
.then(resp => {
|
|
openSnack("EMC set on cable " + cable.name());
|
|
updateComponentCallback(cable.key());
|
|
})
|
|
.catch(err => {});
|
|
};
|
|
|
|
const submit = () => {
|
|
componentAPI
|
|
.update(device.id(), cable.settings, [binKey], ["bin"], as)
|
|
.then(resp => {
|
|
//after updating the component update the interactions with the new subtypes
|
|
//TODO-CS: make function to update multiple interactions at once to avoid this loop if it becomes a problem
|
|
cableInteractions.forEach(interaction => {
|
|
interactionAPI
|
|
.updateInteraction(device.id(), interaction.settings, as)
|
|
.then(resp => {})
|
|
.catch(err => {});
|
|
});
|
|
updateComponentCallback(cable.key())
|
|
})
|
|
.catch(err => {
|
|
openSnack("Failed to update component");
|
|
});
|
|
closeDialog();
|
|
};
|
|
|
|
const updateTopNode = (checked: boolean) => {
|
|
if (topNode && !checked) {
|
|
cable.settings.grainFilledTo = 0;
|
|
cableInteractions.forEach(interaction => {
|
|
interaction.settings.subtype = 0;
|
|
interaction.settings.nodeOne = 0;
|
|
interaction.settings.nodeTwo = 0;
|
|
});
|
|
} else if (checked) {
|
|
cable.settings.grainFilledTo = displayNode;
|
|
cableInteractions.forEach(interaction => {
|
|
interaction.settings.subtype = Interaction.upToSubtype(displayNode);
|
|
interaction.settings.nodeOne = displayNode;
|
|
});
|
|
}
|
|
let pref = pond.BinComponentPreferences.create({
|
|
type: pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE,
|
|
node: cable.settings.grainFilledTo
|
|
});
|
|
//update the bins preferences to have the new top node for the cable
|
|
binAPI
|
|
.updateComponentPreferences(binKey, cable.key(), pref, as)
|
|
.then(resp => {
|
|
openSnack("Updated cables top node");
|
|
updateComponentCallback(cable.key());
|
|
})
|
|
.catch(err => {});
|
|
setTopNode(checked);
|
|
};
|
|
|
|
const updateNodeExclusion = (checked: boolean) => {
|
|
if (checked) {
|
|
cable.settings.excludedNodes.push(selectedNode-1)
|
|
} else {
|
|
cable.settings.excludedNodes.splice(cable.settings.excludedNodes.indexOf(selectedNode - 1), 1)
|
|
}
|
|
setExcluded(checked)
|
|
}
|
|
|
|
const displayTemp = () => {
|
|
let tempFinal = nodeTemp ?? 0;
|
|
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
|
|
tempFinal = tempFinal * 1.8 + 32;
|
|
}
|
|
return tempFinal.toFixed(2);
|
|
};
|
|
|
|
const displayNext = () => {
|
|
let i = displayNode;
|
|
if (i === cable.temperatures.length) {
|
|
i = 1;
|
|
} else {
|
|
i = i + 1;
|
|
}
|
|
setDisplayNode(i);
|
|
};
|
|
|
|
const displayPrev = () => {
|
|
let i = displayNode;
|
|
if (i === 1) {
|
|
i = cable.temperatures.length;
|
|
} else {
|
|
i = i - 1;
|
|
}
|
|
setDisplayNode(i);
|
|
};
|
|
|
|
const latestReading = () => {
|
|
return (
|
|
<Box>
|
|
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Latest Reading</Typography>
|
|
<Typography style={{ fontSize: 10 }}> {moment(cable.lastReading).fromNow()}</Typography>
|
|
<Grid container direction="row" spacing={2} style={{ padding: 10 }}>
|
|
<Grid item xs={6}>
|
|
<Card className={classes.readingCard}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<TemperatureIcon />
|
|
<Typography style={{ color: tempDescriber.colour(), fontWeight: 650 }}>
|
|
{displayTemp() + tempDescriber.GetUnit()}
|
|
</Typography>
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
Temperature
|
|
</Typography>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Card className={classes.readingCard}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<HumidityIcon />
|
|
<Typography style={{ color: humDescriber.colour(), fontWeight: 650 }}>
|
|
{nodeHum + humDescriber.GetUnit()}
|
|
</Typography>
|
|
</Box>
|
|
<Typography style={{ textAlign: "center", color: "grey", fontWeight: 650 }}>
|
|
Humidity
|
|
</Typography>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
{/* only show the emc if the bin grain type and the cable grain type match */}
|
|
{nodeMoist && cable.settings.grainType === grain ? (
|
|
<Card className={classes.readingCard}>
|
|
<Box display="flex" justifyContent="center" alignItems="center">
|
|
<HumidityIcon />
|
|
<Typography style={{ color: moistureDescriber.colour(), fontWeight: 650 }}>
|
|
{nodeMoist + moistureDescriber.GetUnit()}
|
|
</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>
|
|
)}
|
|
</Grid>
|
|
<Grid item xs={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>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const nodeControl = () => {
|
|
return (
|
|
<Box style={{ marginTop: 10 }}>
|
|
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Node Control</Typography>
|
|
<FormControlLabel
|
|
style={{ marginTop: 10 }}
|
|
control={
|
|
<Checkbox
|
|
checked={excluded}
|
|
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={topNode}
|
|
disabled={excluded}
|
|
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 = () => {
|
|
let component = Component.create();
|
|
component.settings = cable.settings;
|
|
component.status = cable.status;
|
|
return (
|
|
<Box marginTop={2}>
|
|
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Interactions</Typography>
|
|
<Box marginTop={1} marginBottom={1}>
|
|
<InteractionsOverview
|
|
component={component}
|
|
components={interactionComponents}
|
|
device={device}
|
|
interactions={cableInteractions}
|
|
permissions={permissions}
|
|
refreshCallback={() => {
|
|
interactionAPI
|
|
.listInteractionsByComponent(device.id(), cable.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={() => closeDialog()} fullWidth={isMobile}>
|
|
<DialogTitle style={{ padding: 10 }}>
|
|
<Box display="flex" justifyContent="center" marginBottom={2}>
|
|
<Typography style={{ fontSize: 30, fontWeight: 650 }}>{cable.name()}</Typography>
|
|
</Box>
|
|
<AppBar className={classes.appBar} color="secondary">
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
justifyContent="space-between"
|
|
alignItems="center"
|
|
wrap="nowrap">
|
|
<Grid item>
|
|
<Button onClick={() => displayPrev()}>
|
|
<ArrowBackIos style={{ color: "black" }} />
|
|
</Button>
|
|
</Grid>
|
|
<Grid item>
|
|
<Box>
|
|
<Typography align="center" variant="h5">
|
|
Node {displayNode}
|
|
</Typography>
|
|
</Box>
|
|
</Grid>
|
|
<Grid item>
|
|
<Button onClick={() => displayNext()}>
|
|
<ArrowForwardIos style={{ color: "black" }} />
|
|
</Button>
|
|
</Grid>
|
|
</Grid>
|
|
</AppBar>
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
{latestReading()}
|
|
{nodeControl()}
|
|
{interactionDisplay()}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={closeDialog} color="primary">
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={submit} color="primary">
|
|
Submit
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
<InteractionSettings
|
|
isDialogOpen={newInteraction}
|
|
initialComponent={cable.asComponent()}
|
|
mode="add"
|
|
device={device}
|
|
components={interactionComponents}
|
|
closeDialogCallback={() => {
|
|
setNewInteraction(false);
|
|
}}
|
|
refreshCallback={() => {
|
|
interactionAPI.listInteractionsByComponent(device.id(), cable.location(), undefined, as).then(resp => {
|
|
setCableInteractions(resp);
|
|
});
|
|
}}
|
|
canEdit={canWrite(permissions)}
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
}
|