finished the form for creating an interaction/alert for a device through an object
This commit is contained in:
parent
ce7c90f384
commit
bb92077d0b
6 changed files with 916 additions and 1005 deletions
|
|
@ -14,11 +14,12 @@ export interface Alert {
|
|||
|
||||
interface Props {
|
||||
alerts: Alert[]
|
||||
permissions: pond.Permission[]
|
||||
addNew?: () => void
|
||||
}
|
||||
|
||||
export default function Alerts(props: Props){
|
||||
const {alerts, addNew} = props
|
||||
const {alerts, addNew, permissions} = props
|
||||
// const [openNewAlert, setOpenNewAlert] = useState(false)
|
||||
|
||||
const readableCondition = (condition: pond.InteractionCondition) => {
|
||||
|
|
@ -64,17 +65,13 @@ export default function Alerts(props: Props){
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* <NewObjectInteraction open={openNewAlert} onClose={(refresh)=>{
|
||||
setOpenNewAlert(false)
|
||||
if(refresh && refreshCallback) refreshCallback()
|
||||
}} typeOptions={typeOptions} linkedComponents={linkedComponents}/> */}
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
|
||||
<Typography>
|
||||
Alerts
|
||||
</Typography>
|
||||
{addNew &&
|
||||
<IconButton onClick={()=>{addNew()}} color="primary"><Add /></IconButton>
|
||||
<IconButton disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)} onClick={()=>{addNew()}} color="primary"><Add /></IconButton>
|
||||
}
|
||||
</ListSubheader>
|
||||
{alerts.map((alert, i) => (
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ export interface Control {
|
|||
interface Props {
|
||||
devices: Device[]
|
||||
controls: Control[]
|
||||
addNew?: (deviceID: number) => void
|
||||
permissions: pond.Permission[]
|
||||
addNew?: (device: Device) => void
|
||||
}
|
||||
|
||||
export default function Controls(props: Props){
|
||||
const { devices, controls, addNew } = props
|
||||
const { devices, controls, addNew, permissions } = props
|
||||
const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map())
|
||||
const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
|
||||
const [anchor, setAnchor] = useState<null | HTMLElement>(null)
|
||||
|
|
@ -62,7 +63,7 @@ export default function Controls(props: Props){
|
|||
onClick={() => {
|
||||
setAnchor(null)
|
||||
setOpenDeviceMenu(false)
|
||||
if (addNew) addNew(dev.id())
|
||||
if (addNew) addNew(dev)
|
||||
|
||||
}}>
|
||||
{dev.name()}
|
||||
|
|
@ -81,6 +82,7 @@ export default function Controls(props: Props){
|
|||
Alerts
|
||||
</Typography>
|
||||
<IconButton
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||
onClick={(e)=>{
|
||||
setOpenDeviceMenu(true)
|
||||
setAnchor(e.currentTarget)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,26 @@
|
|||
import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select } from "@mui/material";
|
||||
import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, FormHelperText, FormLabel, Grid2, IconButton, InputAdornment, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select, Switch, TextField, Theme, Tooltip, Typography } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import classNames from "classnames";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { Option } from "common/SearchSelect";
|
||||
import { Component } from "models";
|
||||
import { capitalize, cloneDeep } from "lodash";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { extension, getMeasurements } from "pbHelpers/ComponentType";
|
||||
import { Measurement, Operator } from "pbHelpers/Enums";
|
||||
import { ComponentType, Measurement, Operator } from "pbHelpers/Enums";
|
||||
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import AddIcon from "@mui/icons-material/AddCircle";
|
||||
import RemoveIcon from "@mui/icons-material/RemoveCircle";
|
||||
import { green, red } from "@mui/material/colors";
|
||||
import { or } from "utils";
|
||||
import moment from "moment";
|
||||
import { timeOfDayDescriptor } from "pbHelpers/Interaction";
|
||||
import { TimePicker } from "@mui/x-date-pickers";
|
||||
import { useInteractionsAPI, useSnackbar } from "hooks";
|
||||
import { useGlobalState } from "providers";
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
|
|
@ -14,11 +28,45 @@ interface Props {
|
|||
linkedComponents: Map<string, Component>
|
||||
componentDevices: Map<string, number>
|
||||
//if we pass in a device id we could filter the components to only be for that device, this could be used for the control part
|
||||
device?: number
|
||||
device?: Device
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return (
|
||||
{
|
||||
borderedContainer: {
|
||||
padding: theme.spacing(1),
|
||||
marginBottom: theme.spacing(2),
|
||||
border: "1px solid rgba(255, 255, 255, 0.12)",
|
||||
borderRadius: "4px"
|
||||
},
|
||||
greenButton: {
|
||||
color: green["600"]
|
||||
},
|
||||
redButton: {
|
||||
color: red["600"]
|
||||
},
|
||||
noPadding: {
|
||||
padding: 0
|
||||
},
|
||||
forPrompt: {
|
||||
fontSize: "0.875em",
|
||||
marginTop: "10px"
|
||||
},
|
||||
timeRange: {
|
||||
fontSize: "0.875em",
|
||||
marginTop: "20px"
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export default function NewObjectInteraction(props: Props){
|
||||
const { open, onClose, device, linkedComponents, componentDevices } = props
|
||||
const classes = useStyles()
|
||||
const [{ as }] = useGlobalState();
|
||||
const {openSnack} = useSnackbar();
|
||||
const interactionsAPI = useInteractionsAPI();
|
||||
const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>(
|
||||
quack.ComponentType.COMPONENT_TYPE_INVALID
|
||||
);
|
||||
|
|
@ -26,14 +74,49 @@ export default function NewObjectInteraction(props: Props){
|
|||
const [selectedAlertComponents, setSelectedAlertComponents] = useState<string[]>([]);
|
||||
const [validOptions, setValidOptions] = useState<Option[]>([])
|
||||
const [validComponents, setValidComponents] = useState<Component[]>([])
|
||||
//condition value strings - so that decimals are easier to type into the field
|
||||
const [valStrings, setValStrings] = useState(["0", "0"]);
|
||||
const [numConditions, setNumConditions] = useState(0)
|
||||
const [nodeOptions, setNodeOptions] = useState<JSX.Element[]>([]);
|
||||
const [subtypeDropdown, setSubtypeDropdown] = useState<number>(0);
|
||||
const [nodeOne, setNodeOne] = useState(0)
|
||||
const [nodeTwo, setNodeTwo] = useState(0)
|
||||
const [maxConditions, setMaxConditions] = useState(2)
|
||||
const [resultType, setResultType] = useState<quack.InteractionResultType>(quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT)
|
||||
const [sinkMotor, setSinkMotor] = useState(false)
|
||||
const [selectedSink, setSelectedSink] = useState("")//the key of the component to be the sink
|
||||
const [sinkOptions, setSinkOptions] = useState<Component[]>([])
|
||||
const [resultMode, setResultMode] = useState(0)
|
||||
const [resultValue, setResultValue] = useState("")
|
||||
const [dutyCycleEnabled, setDutyCycleEnabled] = useState(false)
|
||||
const [dutyCycle, setDutyCycle] = useState<string>("");
|
||||
const [reporting, setReporting] = useState(true)
|
||||
const [notify, setNotify] = useState(true)
|
||||
//schedule variables
|
||||
const [schedule, setSchedule] = useState(
|
||||
pond.InteractionSchedule.create({
|
||||
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
//if the device is passed in need to filter out possible components
|
||||
let validCompList: Component[] = []
|
||||
let validOptions: Option[] = []
|
||||
let sinkOptions: Component[] = []
|
||||
|
||||
if(device){
|
||||
setMaxConditions(device.maxConditions())
|
||||
}
|
||||
|
||||
linkedComponents.forEach((comp, key) => {
|
||||
if(componentDevices.get(key) === device || !device){
|
||||
if(componentDevices.get(key) === device?.id() || !device){
|
||||
let ext = extension(comp.type(), comp.subType())
|
||||
if(ext.isController){
|
||||
sinkOptions.push(comp)
|
||||
}
|
||||
validCompList.push(comp)
|
||||
}
|
||||
})
|
||||
|
|
@ -62,10 +145,59 @@ export default function NewObjectInteraction(props: Props){
|
|||
|
||||
setValidOptions(validOptions)
|
||||
setValidComponents(validCompList)
|
||||
setSinkOptions(sinkOptions)
|
||||
},[device, linkedComponents, componentDevices])
|
||||
|
||||
//use the selected components to determine the lowest amount of nodes for the options
|
||||
useEffect(() => {
|
||||
setNodeOne(0);
|
||||
setNodeTwo(0);
|
||||
setSubtypeDropdown(0);
|
||||
let nodeCount = 12; //start with the maximum number of nodes for a cable
|
||||
validComponents.forEach(comp => {
|
||||
if (selectedAlertComponents.includes(comp.key())) {
|
||||
if (comp.lastMeasurement[0] && comp.lastMeasurement[0].values[0]) {
|
||||
let nodes = comp.lastMeasurement[0].values[0].values.length;
|
||||
nodeCount = nodes < nodeCount ? nodes : nodeCount;
|
||||
}
|
||||
}
|
||||
});
|
||||
let options = [];
|
||||
for (let i = 0; i <= nodeCount; i++) {
|
||||
options.push(
|
||||
<MenuItem key={i} value={i}>
|
||||
{i === 0 ? "None" : "Node " + i}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
setNodeOptions(options);
|
||||
}, [validComponents, selectedAlertComponents]);
|
||||
|
||||
|
||||
const close = (refresh: boolean) => {
|
||||
//reset the state variables to their default values
|
||||
setNewAlertComponentType(quack.ComponentType.COMPONENT_TYPE_INVALID);
|
||||
setConditions([]);
|
||||
setSelectedAlertComponents([]);
|
||||
setValidOptions([])
|
||||
setValidComponents([])
|
||||
setValStrings(["0", "0"]);
|
||||
setNumConditions(0)
|
||||
setNodeOptions([]);
|
||||
setSubtypeDropdown(0);
|
||||
setNodeOne(0)
|
||||
setNodeTwo(0)
|
||||
setMaxConditions(2)
|
||||
setResultType(quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT)
|
||||
setSinkMotor(false)
|
||||
setSelectedSink("")//the key of the component to be the sink
|
||||
setSinkOptions([])
|
||||
setResultMode(0)
|
||||
setResultValue("")
|
||||
setDutyCycleEnabled(false)
|
||||
setDutyCycle("");
|
||||
setReporting(true)
|
||||
setNotify(true)
|
||||
onClose(refresh)
|
||||
}
|
||||
const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
|
||||
|
|
@ -115,6 +247,758 @@ export default function NewObjectInteraction(props: Props){
|
|||
);
|
||||
};
|
||||
|
||||
const measurementTypeMenuItems = () => {
|
||||
return availableMeasurementTypes(newAlertComponentType).map(measurementType => (
|
||||
<MenuItem key={measurementType} value={measurementType}>
|
||||
{describeMeasurement(measurementType).label()}
|
||||
</MenuItem>
|
||||
));
|
||||
};
|
||||
|
||||
const changeCondition = (newCondition: pond.InteractionCondition, index: number) => {
|
||||
let c = conditions;
|
||||
c[index] = newCondition;
|
||||
setConditions([...c]);
|
||||
};
|
||||
|
||||
const conditionGroup = (condition: pond.InteractionCondition, index: number) => {
|
||||
let measurementType = condition.measurementType;
|
||||
let describer = describeMeasurement(measurementType, newAlertComponentType);
|
||||
let isBoolean = condition.measurementType === Measurement.boolean;
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<Grid2
|
||||
key={"interaction"}
|
||||
container
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}>
|
||||
{index > 0 && (
|
||||
<Grid2 size={12}>
|
||||
<Typography align="center">AND</Typography>
|
||||
</Grid2>
|
||||
)}
|
||||
<Grid2 size={{ xs:10, sm:4 }}>
|
||||
<TextField
|
||||
select
|
||||
id="measurementType"
|
||||
required={true}
|
||||
label=""
|
||||
helperText="Measurement"
|
||||
//error={!isMeasurementTypeValid(interaction.settings.conditions[index].measurementType)}
|
||||
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||
value={condition.measurementType}
|
||||
onChange={event => {
|
||||
condition.measurementType = +event.target.value;
|
||||
changeCondition(condition, index);
|
||||
}}
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
{measurementTypeMenuItems()}
|
||||
</TextField>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs:10, sm:3 }}>
|
||||
<TextField
|
||||
select
|
||||
id="comparison"
|
||||
required={true}
|
||||
label=""
|
||||
helperText="Comparator"
|
||||
//disabled={!isSourceValid(interaction.settings.source) || isBoolean || !canEdit}
|
||||
value={condition.comparison}
|
||||
onChange={event => {
|
||||
condition.comparison = +event.target.value;
|
||||
changeCondition(condition, index);
|
||||
}}
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
{!isBoolean && [
|
||||
<MenuItem key={2} value={Operator.less}>
|
||||
{"is below"}
|
||||
</MenuItem>,
|
||||
<MenuItem key={3} value={Operator.greater}>
|
||||
{"is above"}
|
||||
</MenuItem>
|
||||
]}
|
||||
<MenuItem key={1} value={Operator.equals}>
|
||||
{"is exactly"}
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs:8, sm:4}}>
|
||||
<TextField
|
||||
select={isBoolean}
|
||||
id="value"
|
||||
required={true}
|
||||
type={"text"}
|
||||
label=""
|
||||
helperText="Value"
|
||||
error={isNaN(+valStrings[index])}
|
||||
value={valStrings[index]}
|
||||
onChange={event => {
|
||||
let strings = valStrings;
|
||||
strings[index] = event.target.value;
|
||||
setValStrings([...strings]);
|
||||
if (!isNaN(+valStrings[index])) {
|
||||
condition.value = describer.toStored(+event.target.value);
|
||||
changeCondition(condition, index);
|
||||
}
|
||||
}}
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{describer.unit()}</InputAdornment>
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
{isBoolean && [
|
||||
<MenuItem key={0} value={0}>
|
||||
{describer.enumerations()[0]}
|
||||
</MenuItem>,
|
||||
<MenuItem key={1} value={1}>
|
||||
{describer.enumerations()[1]}
|
||||
</MenuItem>
|
||||
]}
|
||||
</TextField>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs:2, sm:1}}>
|
||||
<Tooltip title="Remove this condition">
|
||||
<IconButton
|
||||
color="default"
|
||||
disabled={numConditions === 1}
|
||||
aria-label="Remove condition"
|
||||
onClick={() => {
|
||||
conditions.splice(index, 1)
|
||||
setNumConditions(conditions.length)
|
||||
}}
|
||||
className={classNames(classes.redButton, classes.noPadding)}>
|
||||
<RemoveIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
{nodeOptions.length > 2 && (
|
||||
<Grid2
|
||||
key={"nodes"}
|
||||
container
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={2}>
|
||||
<Grid2 size={{xs:10, sm:4}}>
|
||||
<TextField
|
||||
select
|
||||
id="subtype"
|
||||
label="Subtype"
|
||||
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||
value={subtypeDropdown}
|
||||
onChange={event => {
|
||||
setSubtypeDropdown(+event.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem key={0} value={0}>
|
||||
Any
|
||||
</MenuItem>
|
||||
<MenuItem key={1} value={1}>
|
||||
Average
|
||||
</MenuItem>
|
||||
<MenuItem key={2} value={2}>
|
||||
Single Node
|
||||
</MenuItem>
|
||||
<MenuItem key={3} value={3}>
|
||||
Node Diff
|
||||
</MenuItem>
|
||||
<MenuItem key={4} value={4}>
|
||||
Up To
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs:10, sm:4}}>
|
||||
{(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && (
|
||||
<TextField
|
||||
select
|
||||
id="interactionType"
|
||||
label="Node 1"
|
||||
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||
value={nodeOne}
|
||||
onChange={event => {
|
||||
setNodeOne(+event.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
{nodeOptions}
|
||||
</TextField>
|
||||
)}
|
||||
</Grid2>
|
||||
<Grid2 size={{xs:10, sm:4}}>
|
||||
{subtypeDropdown === 3 && (
|
||||
<TextField
|
||||
select
|
||||
id="interactionType"
|
||||
label="Node 2"
|
||||
//disabled={!isSourceValid(interaction.settings.source) || !canEdit}
|
||||
value={nodeTwo}
|
||||
onChange={event => {
|
||||
setNodeTwo(+event.target.value);
|
||||
}}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
{nodeOptions}
|
||||
</TextField>
|
||||
)}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const conditionInput = () => {
|
||||
return (
|
||||
<FormControl
|
||||
component={"fieldset" as "div"}
|
||||
className={classes.borderedContainer}
|
||||
fullWidth
|
||||
disabled={selectedAlertComponents.length === 0}>
|
||||
<FormLabel component={"legend" as "caption"}>Conditions</FormLabel>
|
||||
{selectedAlertComponents.length > 0 ? (
|
||||
<React.Fragment>
|
||||
{conditions.map((condition, i) => conditionGroup(condition, i))}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<FormHelperText>You must select a source before adding conditions</FormHelperText>
|
||||
)}
|
||||
<Tooltip title="Add another condition">
|
||||
<IconButton
|
||||
color="primary"
|
||||
disabled={numConditions >= maxConditions}
|
||||
aria-label="Add another condition"
|
||||
onClick={() => {
|
||||
let newConditions = conditions.concat(initialConditions(newAlertComponentType));
|
||||
setConditions(newConditions);
|
||||
setNumConditions(newConditions.length)
|
||||
}}
|
||||
className={classNames(classes.greenButton, classes.noPadding)}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
const sinkSelector = () => {
|
||||
return (
|
||||
<TextField
|
||||
select
|
||||
id="sink"
|
||||
label=""
|
||||
value={selectedSink}
|
||||
onChange={(e) => {
|
||||
setSelectedSink(e.target.value)
|
||||
setSinkMotor(linkedComponents.get(e.target.value)?.type() === ComponentType.stepperMotor)
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem key={""} value={""}>
|
||||
Select Controller
|
||||
</MenuItem>
|
||||
{sinkOptions.map(comp => (
|
||||
<MenuItem key={comp.key()} value={comp.key()}>
|
||||
{comp.name()}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
|
||||
const describeSink = (): MeasurementDescriber => {
|
||||
const sink = linkedComponents.get(selectedSink);
|
||||
return describeMeasurement(
|
||||
Measurement.boolean,
|
||||
or(sink, Component.create()).settings.type,
|
||||
or(sink, Component.create()).settings.subtype
|
||||
);
|
||||
};
|
||||
|
||||
const dutyCycleInput = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid2 size={{ xs: 4, sm: 4 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
id="enableDutyCycle"
|
||||
checked={dutyCycleEnabled}
|
||||
onChange={(_, checked) => {
|
||||
setDutyCycleEnabled(checked)
|
||||
}}
|
||||
value="enableDutyCycle"
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
label="Once every"
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 8, sm: 8 }}>
|
||||
<TextField
|
||||
id="dutyCycle"
|
||||
value={dutyCycle}
|
||||
onChange={(e) => {
|
||||
setDutyCycle(e.target.value)
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
disabled={!dutyCycleEnabled}
|
||||
error={isNaN(Number(dutyCycle)) || Number(dutyCycle) < 0}
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
|
||||
}}></TextField>
|
||||
</Grid2>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const notificationInput = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT && (
|
||||
<Grid2 size={{xs:6, sm:3}}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
id="notificationReports"
|
||||
checked={reporting}
|
||||
onChange={(_, checked) => {
|
||||
setReporting(checked)
|
||||
}}
|
||||
value="notificationReports"
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
label="Report"
|
||||
/>
|
||||
</Grid2>
|
||||
)}
|
||||
<Grid2 size={{xs: resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE ? 6 : 12, sm: 3}} >
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
id="interactionNotification"
|
||||
checked={notify}
|
||||
onChange={(_, checked) => {
|
||||
setNotify(checked)
|
||||
}}
|
||||
value="interactionNotification"
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
label="Notify"
|
||||
disabled={!reporting}
|
||||
/>
|
||||
</Grid2>
|
||||
{notify && (
|
||||
<Grid2 size={{ xs: 12, sm: 12 }}>
|
||||
<Typography color="textSecondary" variant="subtitle1">
|
||||
Everyone with notifications enabled for this device will receive an email and/or SMS
|
||||
each time this result occurs
|
||||
</Typography>
|
||||
</Grid2>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const resultInput = () => {
|
||||
return (
|
||||
<FormControl component={"fieldset" as "div"} className={classes.borderedContainer} fullWidth>
|
||||
<FormLabel component={"legend" as "caption"}>Result</FormLabel>
|
||||
<Grid2 container direction="row" spacing={2}>
|
||||
<Grid2 size={{ xs: 12, sm: 3 }}>
|
||||
<TextField
|
||||
select
|
||||
id="resultType"
|
||||
required={true}
|
||||
label=""
|
||||
value={resultType}
|
||||
onChange={(e) => {
|
||||
setResultType(+e.target.value as quack.InteractionResultType)
|
||||
}}
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem value={quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT}>Report</MenuItem>
|
||||
<MenuItem value={quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET}>Set</MenuItem>
|
||||
<MenuItem value={quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE}>Toggle</MenuItem>
|
||||
<MenuItem value={quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN}>Run</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
{resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN && (
|
||||
<React.Fragment>
|
||||
<Grid2 size={{ xs: 12, sm: sinkMotor ? 8 : 4 }}>
|
||||
{sinkSelector()}
|
||||
</Grid2>
|
||||
{sinkMotor && (
|
||||
<Grid2 size={{ xs: 12, sm: 5 }} container justifyContent="center">
|
||||
<TextField
|
||||
select
|
||||
id="mode"
|
||||
label=""
|
||||
value={resultMode}
|
||||
onChange={(e) => {
|
||||
setResultMode(+e.target.value)
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem key={"Clockwise"} value={0}>
|
||||
Clockwise
|
||||
</MenuItem>
|
||||
<MenuItem key={"Counter Clockwise"} value={1}>
|
||||
Counter Clockwise
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
)}
|
||||
<Grid2 size={{ xs: 2, sm: 1 }}>
|
||||
<Typography variant="subtitle1" color="textSecondary" className={classes.forPrompt}>
|
||||
for
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 10, sm: sinkMotor ? 6 : 4 }} container justifyContent="center">
|
||||
<TextField
|
||||
id="runDuration"
|
||||
required={true}
|
||||
label=""
|
||||
helperText=""
|
||||
error={isNaN(parseFloat(resultValue))}
|
||||
value={resultValue}
|
||||
onChange={(e) => {
|
||||
setResultValue(e.target.value)
|
||||
}}
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">sec</InputAdornment>
|
||||
}}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
</Grid2>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET && (
|
||||
<React.Fragment>
|
||||
<Grid2 size={{ xs: 12, sm: 4 }}>
|
||||
{sinkSelector()}
|
||||
</Grid2>
|
||||
{linkedComponents.get(selectedSink)?.type() !==
|
||||
quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
|
||||
<Grid2 size={{ xs: 2, sm: 1 }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color="textSecondary"
|
||||
className={classes.forPrompt}>
|
||||
to
|
||||
</Typography>
|
||||
</Grid2>
|
||||
)}
|
||||
{linkedComponents.get(selectedSink)?.type() !==
|
||||
quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
|
||||
<Grid2 size={{ xs: 10, sm: 3 }} container justifyContent="center">
|
||||
<TextField
|
||||
select
|
||||
id="resultValue"
|
||||
label=""
|
||||
value={resultValue}
|
||||
onChange={(e) => {
|
||||
setResultValue(e.target.value)
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus={false}
|
||||
margin="dense"
|
||||
variant="standard"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
{extension(linkedComponents.get(selectedSink)?.type() ?? quack.ComponentType.COMPONENT_TYPE_INVALID).states.map((state, i) => (
|
||||
<MenuItem key={state} value={i}>
|
||||
{state}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Grid2>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
{resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE && (
|
||||
<React.Fragment>
|
||||
<Grid2 size={{ xs: 12, sm: 5 }}>
|
||||
{sinkSelector()}
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: 3 }} container justifyContent="center">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
id="resultValue"
|
||||
checked={parseFloat(resultValue) === 1}
|
||||
onChange={(_, checked) => {
|
||||
if(checked){
|
||||
setResultValue('1')
|
||||
}else{
|
||||
setResultValue('0')
|
||||
}
|
||||
}}
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
label={describeSink().enumerations()[isNaN(parseFloat(resultValue))?0:parseFloat(resultValue)]}
|
||||
/>
|
||||
</Grid2>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN &&
|
||||
dutyCycleInput()}
|
||||
{notificationInput()}
|
||||
</Grid2>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleDaySelected = (day: string, event: any) => {
|
||||
let updatedSchedule = cloneDeep(schedule);
|
||||
if (event.target.checked) {
|
||||
if (!updatedSchedule.weekdays.includes(day)) {
|
||||
updatedSchedule.weekdays.push(day);
|
||||
}
|
||||
} else {
|
||||
updatedSchedule.weekdays.splice(updatedSchedule.weekdays.indexOf(day), 1);
|
||||
}
|
||||
setSchedule(updatedSchedule);
|
||||
};
|
||||
|
||||
const daySelector = (day: string, size: any) => {
|
||||
//const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
|
||||
return (
|
||||
<Grid2 container size={{xs: size, sm:1}} alignItems="center">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
id={day}
|
||||
checked={schedule.weekdays.includes(day)}
|
||||
onChange={event => toggleDaySelected(day, event)}
|
||||
value={day}
|
||||
color="secondary"
|
||||
/>
|
||||
}
|
||||
label={capitalize(day.substr(0, 2))}
|
||||
labelPlacement="bottom"
|
||||
/>
|
||||
</Grid2>
|
||||
);
|
||||
};
|
||||
|
||||
const setScheduleTime = (id: string, event: any) => {
|
||||
let updatedSchedule = cloneDeep(schedule);
|
||||
if (id === "shortcut") {
|
||||
let value = event.target.value;
|
||||
if (value === "allDay") {
|
||||
updatedSchedule.timeOfDayStart = "00:00";
|
||||
updatedSchedule.timeOfDayEnd = "24:00";
|
||||
} else if (value === "before") {
|
||||
updatedSchedule.timeOfDayStart = "00:00";
|
||||
if (!updatedSchedule.timeOfDayEnd || updatedSchedule.timeOfDayEnd === "24:00") {
|
||||
updatedSchedule.timeOfDayEnd = "23:59";
|
||||
}
|
||||
} else if (value === "after") {
|
||||
updatedSchedule.timeOfDayEnd = "24:00";
|
||||
if (updatedSchedule.timeOfDayStart === "00:00") {
|
||||
updatedSchedule.timeOfDayStart = "00:01";
|
||||
}
|
||||
} else {
|
||||
if (updatedSchedule.timeOfDayStart === "00:00") {
|
||||
updatedSchedule.timeOfDayStart = "00:01";
|
||||
}
|
||||
if (updatedSchedule.timeOfDayEnd === "24:00") {
|
||||
updatedSchedule.timeOfDayEnd = "23:59";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let timeOfDay =
|
||||
event
|
||||
.hour()
|
||||
.toString()
|
||||
.padStart(2, "0") +
|
||||
":" +
|
||||
event
|
||||
.minute()
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
if (id === "start") {
|
||||
updatedSchedule.timeOfDayStart = timeOfDay;
|
||||
} else if (id === "end") {
|
||||
if (timeOfDay === "00:00") {
|
||||
timeOfDay = "24:00";
|
||||
}
|
||||
updatedSchedule.timeOfDayEnd = timeOfDay;
|
||||
}
|
||||
}
|
||||
updatedSchedule.timezone = moment.tz.guess();
|
||||
setSchedule(updatedSchedule);
|
||||
};
|
||||
|
||||
const scheduler = () => {
|
||||
//let schedule = pond.InteractionSchedule.create();
|
||||
let timezone = moment.tz.guess();
|
||||
let todStart = "00:00".split(":");
|
||||
let todEnd = "23:59".split(":");
|
||||
let start = moment
|
||||
.tz(timezone)
|
||||
.startOf("day")
|
||||
.add(todStart[0], "hours")
|
||||
.add(todStart[1], "minutes")
|
||||
.local();
|
||||
let end = moment
|
||||
.tz(timezone)
|
||||
.startOf("day")
|
||||
.add(todEnd[0], "hours")
|
||||
.add(todEnd[1], "minutes")
|
||||
.local();
|
||||
let descriptor = timeOfDayDescriptor(schedule);
|
||||
let showStart = descriptor === "after" || descriptor === "from";
|
||||
let showEnd = descriptor === "before" || descriptor === "from";
|
||||
let showBoth = showStart && showEnd;
|
||||
return (
|
||||
<FormControl component={"fieldset" as "div"} className={classes.borderedContainer} fullWidth>
|
||||
<FormLabel component={"legend" as "caption"}>Schedule</FormLabel>
|
||||
<Grid2 container direction="row" spacing={0}>
|
||||
{daySelector("sunday", 3)}
|
||||
{daySelector("monday", 3)}
|
||||
{daySelector("tuesday", 3)}
|
||||
{daySelector("wednesday", 3)}
|
||||
{daySelector("thursday", 4)}
|
||||
{daySelector("friday", 4)}
|
||||
{daySelector("saturday", 4)}
|
||||
</Grid2>
|
||||
<Grid2 container direction="row" spacing={2} alignItems="center">
|
||||
<Grid2 size={{xs:12, sm:3}}>
|
||||
<TextField
|
||||
select
|
||||
id="timeOfDayShortcut"
|
||||
value={descriptor}
|
||||
onChange={(event: any) => setScheduleTime("shortcut", event)}
|
||||
fullWidth
|
||||
autoFocus={false}
|
||||
margin="normal"
|
||||
variant="standard"
|
||||
InputLabelProps={{ shrink: true }}>
|
||||
<MenuItem key="allDay" value="allDay">
|
||||
All Day
|
||||
</MenuItem>
|
||||
<MenuItem key="before" value="before">
|
||||
Before
|
||||
</MenuItem>
|
||||
<MenuItem key="after" value="after">
|
||||
After
|
||||
</MenuItem>
|
||||
<MenuItem key="from" value="from">
|
||||
From
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
</Grid2>
|
||||
{showStart && (
|
||||
<Grid2 size={{xs:5, sm:3}}>
|
||||
<TimePicker
|
||||
//renderInput={props => <TextField {...props} helperText="" />}
|
||||
value={start}
|
||||
onChange={(event: any) => setScheduleTime("start", event)}
|
||||
/>
|
||||
</Grid2>
|
||||
)}
|
||||
{showBoth && (
|
||||
<Grid2 size={{xs:2,sm:1}}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
color="textSecondary"
|
||||
className={classes.timeRange}
|
||||
align="right">
|
||||
to
|
||||
</Typography>
|
||||
</Grid2>
|
||||
)}
|
||||
{showEnd && (
|
||||
<Grid2 size={{xs:5, sm:3}}>
|
||||
<TimePicker
|
||||
//renderInput={props => <TextField {...props} helperText="" />}
|
||||
value={end}
|
||||
onChange={(event: any) => setScheduleTime("end", event)}
|
||||
/>
|
||||
</Grid2>
|
||||
)}
|
||||
</Grid2>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
const addInteractionToComponents = () => {
|
||||
//array of device and component ids ie, [4:9-5-12, 4:9-5-13, 3:9-5-12]
|
||||
let compIds: string[] = [];
|
||||
selectedAlertComponents.forEach(comp => {
|
||||
let devID = componentDevices.get(comp);
|
||||
let component = linkedComponents.get(comp);
|
||||
if (devID && component) {
|
||||
compIds.push(devID + ":" + component.locationString());
|
||||
}
|
||||
});
|
||||
let newInteraction = pond.InteractionSettings.create();
|
||||
newInteraction.conditions = conditions;
|
||||
newInteraction.instance = 1;
|
||||
newInteraction.schedule = schedule;
|
||||
newInteraction.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo);
|
||||
newInteraction.sink = linkedComponents.get(selectedSink)?.location()
|
||||
newInteraction.result = pond.InteractionResult.create({
|
||||
type: resultType,
|
||||
mode: resultMode,
|
||||
value: isNaN(parseFloat(resultValue)) ? 0 : parseFloat(resultValue),
|
||||
dutyCycle: isNaN(parseFloat(dutyCycle)) ? 0 : parseFloat(dutyCycle)
|
||||
});
|
||||
newInteraction.notifications = pond.InteractionNotifications.create({
|
||||
reports: reporting,
|
||||
notify: reporting && notify
|
||||
});
|
||||
interactionsAPI
|
||||
.addInteractionToComponents(compIds, newInteraction, as)
|
||||
.then(_resp => {
|
||||
openSnack("interaction added to selected components");
|
||||
})
|
||||
.catch(_err => {
|
||||
openSnack("there was a problem adding the interaction to the selected components");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
|
|
@ -144,24 +1028,22 @@ export default function NewObjectInteraction(props: Props){
|
|||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{/* list of checkboxes for the components that match that type */}
|
||||
{listMatchingComponents()}
|
||||
{/* have the conditions set here */}
|
||||
{/* {conditionInput()} */}
|
||||
{/* have the schedule set here */}
|
||||
{/* {scheduler()} */}
|
||||
{conditionInput()}
|
||||
{device && resultInput()}
|
||||
{scheduler()}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => close(false)} variant="contained">
|
||||
Cancel
|
||||
</Button>
|
||||
{/* <Button
|
||||
disabled={selectedAlertComponents.length === 0}
|
||||
<Button
|
||||
//disabled={selectedAlertComponents.length === 0}
|
||||
onClick={addInteractionToComponents}
|
||||
variant="contained"
|
||||
color="primary">
|
||||
Confirm
|
||||
</Button> */}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ interface Props {
|
|||
objectKey: string
|
||||
objectType: pond.ObjectType
|
||||
devices: Device[];
|
||||
permissions: pond.Permission[]
|
||||
refreshCallback?: () => {}
|
||||
}
|
||||
|
||||
export default function ObjectInteractions(props: Props) {
|
||||
const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback } = props
|
||||
const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback, permissions } = props
|
||||
const [{as}] = useGlobalState()
|
||||
//list of alerts, each alert contains a list of interactions with matching conditions
|
||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||
|
|
@ -34,7 +35,7 @@ export default function ObjectInteractions(props: Props) {
|
|||
const [openNewInteraction, setOpenNewInteraction] = useState(false)
|
||||
const [allSinks, setAllSinks] = useState<Component[]>([]);
|
||||
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number | undefined>(undefined)
|
||||
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>(undefined)
|
||||
|
||||
|
||||
const matchConditions = (interaction: Interaction, set: Alert | Control) => {
|
||||
|
|
@ -178,9 +179,7 @@ export default function ObjectInteractions(props: Props) {
|
|||
});
|
||||
setAllSinks(sinkOptions);
|
||||
setAlerts(alerts);
|
||||
console.log(alerts)
|
||||
setControls(controls);
|
||||
console.log(controls)
|
||||
}).catch(err => {
|
||||
console.error("Interaction fetch error:", err)
|
||||
})
|
||||
|
|
@ -206,15 +205,19 @@ export default function ObjectInteractions(props: Props) {
|
|||
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
|
||||
<Controls
|
||||
controls={controls}
|
||||
permissions={permissions}
|
||||
devices={devices}
|
||||
addNew={(deviceID) => {
|
||||
setSelectedDevice(deviceID)
|
||||
addNew={(device) => {
|
||||
setSelectedDevice(device)
|
||||
setOpenNewInteraction(true)
|
||||
}}/>
|
||||
<Alerts alerts={alerts} addNew={() => {
|
||||
<Alerts
|
||||
alerts={alerts}
|
||||
permissions={permissions}
|
||||
addNew={() => {
|
||||
setSelectedDevice(undefined)
|
||||
setOpenNewInteraction(true)
|
||||
}}/>
|
||||
}}/>
|
||||
<Notifications objectKey={objectKey} objectType={objectType}/>
|
||||
</Box>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue