1051 lines
No EOL
36 KiB
TypeScript
1051 lines
No EOL
36 KiB
TypeScript
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 { capitalize, cloneDeep } from "lodash";
|
|
import { Component, Device, Interaction } from "models";
|
|
import { extension, getMeasurements } from "pbHelpers/ComponentType";
|
|
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
|
|
onClose: (refreshCallback: boolean) => void
|
|
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?: 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
|
|
);
|
|
const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]);
|
|
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?.id() || !device){
|
|
let ext = extension(comp.type(), comp.subType())
|
|
if(ext.isController){
|
|
sinkOptions.push(comp)
|
|
}
|
|
validCompList.push(comp)
|
|
}
|
|
})
|
|
|
|
let included: quack.ComponentType[] = []
|
|
validCompList.forEach(comp => {
|
|
if(included.includes(comp.type())) return
|
|
let ext = extension(comp.type(), comp.subType())
|
|
if(device){
|
|
if(!ext.isController){
|
|
included.push(comp.type())
|
|
validOptions.push({
|
|
label: ext.friendlyName,
|
|
value: comp.type()
|
|
})
|
|
}
|
|
}else{
|
|
included.push(comp.type())
|
|
validOptions.push({
|
|
label: ext.friendlyName,
|
|
value: comp.type()
|
|
})
|
|
}
|
|
|
|
})
|
|
|
|
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([]);
|
|
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[] => {
|
|
return getMeasurements(type).map(m => m.measurementType);
|
|
};
|
|
|
|
const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => {
|
|
let measurementType = availableMeasurementTypes(type)[0];
|
|
let condition = pond.InteractionCondition.create({
|
|
measurementType: measurementType,
|
|
comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less,
|
|
value: 0
|
|
});
|
|
return [condition];
|
|
};
|
|
|
|
//display the components that match the type that was selected for the new alert
|
|
const listMatchingComponents = () => {
|
|
let matching: Component[] = [];
|
|
validComponents.forEach(comp => {
|
|
if (comp.type() === newAlertComponentType) {
|
|
matching.push(comp);
|
|
}
|
|
});
|
|
return (
|
|
<List>
|
|
{matching.map(comp => (
|
|
<ListItem key={comp.key()}>
|
|
<ListItemIcon>
|
|
<Checkbox
|
|
checked={selectedAlertComponents.includes(comp.key())}
|
|
onChange={(_, checked) => {
|
|
let selected = selectedAlertComponents;
|
|
if (checked) {
|
|
selected.push(comp.key());
|
|
} else {
|
|
selected.splice(selected.indexOf(comp.key()), 1);
|
|
}
|
|
setSelectedAlertComponents([...selected]);
|
|
}}
|
|
/>
|
|
</ListItemIcon>
|
|
<ListItemText>{comp.name()}</ListItemText>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
);
|
|
};
|
|
|
|
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");
|
|
}).finally(() => {
|
|
close(true)
|
|
});
|
|
};
|
|
|
|
return (
|
|
<ResponsiveDialog
|
|
open={open}
|
|
onClose={() => {
|
|
close(false);
|
|
}}>
|
|
<DialogTitle>Set New Alert</DialogTitle>
|
|
<DialogContent>
|
|
{/* Dropdown to select the component type to add the interaction to */}
|
|
<Select
|
|
id="componentType"
|
|
label="Component Type"
|
|
fullWidth
|
|
displayEmpty
|
|
value={newAlertComponentType}
|
|
onChange={e => {
|
|
setNewAlertComponentType(e.target.value as quack.ComponentType);
|
|
setSelectedAlertComponents([]);
|
|
setConditions(initialConditions(e.target.value as quack.ComponentType));
|
|
}}>
|
|
<MenuItem key={0} value={0}>
|
|
Select Component Type
|
|
</MenuItem>
|
|
{validOptions.map(option => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{listMatchingComponents()}
|
|
{conditionInput()}
|
|
{device && resultInput()}
|
|
{scheduler()}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => close(false)} variant="contained">
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
//disabled={selectedAlertComponents.length === 0}
|
|
onClick={addInteractionToComponents}
|
|
variant="contained"
|
|
color="primary">
|
|
Confirm
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
)
|
|
} |