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 componentDevices: Map //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.COMPONENT_TYPE_INVALID ); const [conditions, setConditions] = useState([]); const [selectedAlertComponents, setSelectedAlertComponents] = useState([]); const [validOptions, setValidOptions] = useState([]) const [validComponents, setValidComponents] = useState([]) //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([]); const [subtypeDropdown, setSubtypeDropdown] = useState(0); const [nodeOne, setNodeOne] = useState(0) const [nodeTwo, setNodeTwo] = useState(0) const [maxConditions, setMaxConditions] = useState(2) const [resultType, setResultType] = useState(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([]) const [resultMode, setResultMode] = useState(0) const [resultValue, setResultValue] = useState("") const [dutyCycleEnabled, setDutyCycleEnabled] = useState(false) const [dutyCycle, setDutyCycle] = useState(""); 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( {i === 0 ? "None" : "Node " + i} ); } 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 ( {matching.map(comp => ( { let selected = selectedAlertComponents; if (checked) { selected.push(comp.key()); } else { selected.splice(selected.indexOf(comp.key()), 1); } setSelectedAlertComponents([...selected]); }} /> {comp.name()} ))} ); }; const measurementTypeMenuItems = () => { return availableMeasurementTypes(newAlertComponentType).map(measurementType => ( {describeMeasurement(measurementType).label()} )); }; 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 ( {index > 0 && ( AND )} { condition.measurementType = +event.target.value; changeCondition(condition, index); }} autoFocus={false} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {measurementTypeMenuItems()} { condition.comparison = +event.target.value; changeCondition(condition, index); }} autoFocus={false} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {!isBoolean && [ {"is below"} , {"is above"} ]} {"is exactly"} { 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: {describer.unit()} }} InputLabelProps={{ shrink: true }}> {isBoolean && [ {describer.enumerations()[0]} , {describer.enumerations()[1]} ]} { conditions.splice(index, 1) setNumConditions(conditions.length) }} className={classNames(classes.redButton, classes.noPadding)}> {nodeOptions.length > 2 && ( { setSubtypeDropdown(+event.target.value); }} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> Any Average Single Node Node Diff Up To {(subtypeDropdown === 2 || subtypeDropdown === 3 || subtypeDropdown === 4) && ( { setNodeOne(+event.target.value); }} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {nodeOptions} )} {subtypeDropdown === 3 && ( { setNodeTwo(+event.target.value); }} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> {nodeOptions} )} )} ); }; const conditionInput = () => { return ( Conditions {selectedAlertComponents.length > 0 ? ( {conditions.map((condition, i) => conditionGroup(condition, i))} ) : ( You must select a source before adding conditions )} = maxConditions} aria-label="Add another condition" onClick={() => { let newConditions = conditions.concat(initialConditions(newAlertComponentType)); setConditions(newConditions); setNumConditions(newConditions.length) }} className={classNames(classes.greenButton, classes.noPadding)}> ); }; const sinkSelector = () => { return ( { setSelectedSink(e.target.value) setSinkMotor(linkedComponents.get(e.target.value)?.type() === ComponentType.stepperMotor) }} fullWidth autoFocus={false} margin="dense" variant="standard" InputLabelProps={{ shrink: true }}> Select Controller {sinkOptions.map(comp => ( {comp.name()} ))} ); } 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 ( { setDutyCycleEnabled(checked) }} value="enableDutyCycle" color="secondary" /> } label="Once every" /> { 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: seconds }}> ); }; const notificationInput = () => { return ( {resultType !== quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT && ( { setReporting(checked) }} value="notificationReports" color="secondary" /> } label="Report" /> )} { setNotify(checked) }} value="interactionNotification" color="secondary" /> } label="Notify" disabled={!reporting} /> {notify && ( Everyone with notifications enabled for this device will receive an email and/or SMS each time this result occurs )} ); }; const resultInput = () => { return ( Result { setResultType(+e.target.value as quack.InteractionResultType) }} autoFocus={false} margin="dense" variant="standard" fullWidth InputLabelProps={{ shrink: true }}> Report Set Toggle Run {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN && ( {sinkSelector()} {sinkMotor && ( { setResultMode(+e.target.value) }} fullWidth autoFocus={false} margin="dense" variant="standard" InputLabelProps={{ shrink: true }}> Clockwise Counter Clockwise )} for { setResultValue(e.target.value) }} autoFocus={false} margin="dense" variant="standard" fullWidth InputProps={{ endAdornment: sec }} InputLabelProps={{ shrink: true }} /> )} {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_SET && ( {sinkSelector()} {linkedComponents.get(selectedSink)?.type() !== quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && ( to )} {linkedComponents.get(selectedSink)?.type() !== quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && ( { 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) => ( {state} ))} )} )} {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE && ( {sinkSelector()} { if(checked){ setResultValue('1') }else{ setResultValue('0') } }} color="secondary" /> } label={describeSink().enumerations()[isNaN(parseFloat(resultValue))?0:parseFloat(resultValue)]} /> )} {resultType === quack.InteractionResultType.INTERACTION_RESULT_TYPE_RUN && dutyCycleInput()} {notificationInput()} ); }; 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 ( toggleDaySelected(day, event)} value={day} color="secondary" /> } label={capitalize(day.substr(0, 2))} labelPlacement="bottom" /> ); }; 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 ( Schedule {daySelector("sunday", 3)} {daySelector("monday", 3)} {daySelector("tuesday", 3)} {daySelector("wednesday", 3)} {daySelector("thursday", 4)} {daySelector("friday", 4)} {daySelector("saturday", 4)} setScheduleTime("shortcut", event)} fullWidth autoFocus={false} margin="normal" variant="standard" InputLabelProps={{ shrink: true }}> All Day Before After From {showStart && ( } value={start} onChange={(event: any) => setScheduleTime("start", event)} /> )} {showBoth && ( to )} {showEnd && ( } value={end} onChange={(event: any) => setScheduleTime("end", event)} /> )} ); }; 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 ( { close(false); }}> Set New Alert {/* Dropdown to select the component type to add the interaction to */} {listMatchingComponents()} {conditionInput()} {device && resultInput()} {scheduler()} ) }