frontend/src/interactions/InteractionSettings.tsx

1611 lines
54 KiB
TypeScript

import {
Button,
Checkbox,
DialogActions,
DialogContent,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
FormHelperText,
FormLabel,
Grid2 as Grid,
IconButton,
InputAdornment,
MenuItem,
Switch,
TextField,
Theme,
Tooltip,
Typography,
useTheme
} from "@mui/material";
import AddIcon from "@mui/icons-material/AddCircle";
import RemoveIcon from "@mui/icons-material/RemoveCircle";
import classNames from "classnames";
import DeleteButton from "common/DeleteButton";
import ResponsiveDialog from "common/ResponsiveDialog";
import { coalesce, or, capitalize } from "utils";
import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks";
import { Component, Device, Interaction } from "models";
import moment from "moment-timezone";
import {
componentIDToString,
emptyComponentId,
getComponentIDString,
sameComponentID,
stringToComponentId
} from "pbHelpers/Component";
import {
extension,
getMeasurements,
GetNumNodes,
hasInteractionResultType,
isSource
} from "pbHelpers/ComponentType";
import { ComponentType, Measurement, Operator, Result } from "pbHelpers/Enums";
import {
getDefaultInteraction,
isInteractionValid,
isMeasurementTypeValid,
isSourceValid,
timeOfDayDescriptor
} from "pbHelpers/Interaction";
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { hasDeviceFeature } from "services/feature/service";
import RemoveInteraction from "./RemoveInteraction";
import { makeStyles } from "@mui/styles";
import { green, red } from "@mui/material/colors";
import { LocalizationProvider, TimePicker } from "@mui/x-date-pickers";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
// import dayjs from "dayjs";
import CancelSubmit from "common/CancelSubmit";
import { Sensor } from "ventilation/drawable/Sensor";
// import { Sensor } from "ventilation/drawable/Sensor";
const useStyles = makeStyles((theme: Theme) => {
return ({
dialogContent: {
marginTop: theme.spacing(1)
},
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"
},
singleInput: {
minHeight: "48px"
}
})
});
interface Props {
device: Device;
initialComponent?: Component;
initialInteraction?: Interaction;
components: Component[];
mode?: string;
isDialogOpen: boolean;
closeDialogCallback: Function;
refreshCallback: Function;
canEdit?: boolean;
sensor?: Sensor;
}
export default function InteractionSettings(props: Props) {
const {
device,
initialComponent,
initialInteraction,
components,
mode,
isDialogOpen,
closeDialogCallback,
refreshCallback,
canEdit,
sensor
} = props;
const theme = useTheme();
const { success, error } = useSnackbar();
const prevInitialInteraction = usePrevious(initialInteraction);
const prevComponents = usePrevious(components);
const prevInitialComponent = usePrevious(initialComponent);
const prevIsDialogOpen = usePrevious(isDialogOpen);
const classes = useStyles();
const [{ user, as }] = useGlobalState();
const interactionsAPI = useInteractionsAPI();
const [interaction, setInteraction] = useState<Interaction>(
Interaction.clone(initialInteraction)
);
const [isRemoveInteractionOpen, setIsRemoveInteractionOpen] = useState<boolean>(false);
const [mappedComponents, setMappedComponents] = useState<Map<string, Component>>(new Map());
const [numConditions, setNumConditions] = useState<number>(0);
const [rawConditionValues, setRawConditionValues] = useState<string[]>([]);
const [rawResultValue, setRawResultValue] = useState<string>("");
const [dutyCycleEnabled, setDutyCycleEnabled] = useState<boolean>(false);
const [dutyCycle, setDutyCycle] = useState<string>("");
const [timezone, setTimezone] = useState<string | undefined>(undefined);
//whether is any node(0), average of all nodes(1), single node(2), or node diff(3)
const [subtypeDropdown, setSubtypeDropdown] = useState<number>(0);
const initialConditions = useCallback(
(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];
},
[]
);
const setDefaultState = useCallback(() => {
let interaction = getDefaultInteraction();
if (initialInteraction && mode === "update") {
interaction = initialInteraction;
if (interaction.settings.subtype === 0 || interaction.settings.subtype === 1) {
setSubtypeDropdown(interaction.settings.subtype);
} else {
if (interaction.settings.subtype >= 138) {
setSubtypeDropdown(4);
} else if (interaction.settings.subtype > 17) {
setSubtypeDropdown(3);
} else if (interaction.settings.subtype > 1) {
setSubtypeDropdown(2);
}
}
}
if (initialComponent && mode === "add") {
interaction.settings.result = pond.InteractionResult.create({
type: Result.report,
value: 0,
mode: 0
});
if (isSource(initialComponent.settings.type)) {
interaction.settings.source = initialComponent.location();
interaction.settings.conditions = initialConditions(initialComponent.settings.type);
}
}
let rawConditionValues = [] as Array<string>;
interaction.settings.conditions.forEach((condition: pond.InteractionCondition) => {
let value = describeMeasurement(
condition.measurementType,
or(initialComponent, Component.create()).settings.type,
or(initialComponent, Component.create()).settings.subtype,
undefined,
user
).toDisplay(condition.value);
rawConditionValues.push(value.toString());
});
let numConditions = interaction.settings.conditions.length;
let mappedComponents: Map<string, Component> = new Map();
components.forEach(component => mappedComponents.set(component.locationString(), component));
let interactionResult = pond.InteractionResult.create(
or(interaction.settings.result, undefined)
);
if (sensor) {
interaction.settings.subtype = 2;
interaction.settings.nodeOne = sensor.index;
interaction.settings.nodeTwo = sensor.index;
}
setIsRemoveInteractionOpen(false);
setNumConditions(numConditions);
setRawConditionValues(rawConditionValues);
setRawResultValue(interactionResult.value.toString());
setDutyCycleEnabled(interactionResult.dutyCycle > 0);
setDutyCycle(interactionResult.dutyCycle.toString());
setMappedComponents(mappedComponents);
setInteraction(interaction);
}, [components, initialComponent, initialConditions, initialInteraction, mode, /*sensor*/]);
const availableSources = () => {
return components.filter(component => isSource(component.settings.type));
};
useEffect(() => {
if (user && user.settings.timezone) {
setTimezone(user.settings.timezone);
}
if (isDialogOpen && prevIsDialogOpen) return;
if (
prevInitialInteraction !== initialInteraction ||
(prevComponents && prevComponents.length !== components.length) ||
getComponentIDString(prevInitialComponent) !== getComponentIDString(initialComponent) ||
isDialogOpen !== prevIsDialogOpen
) {
setDefaultState();
}
}, [
components.length,
initialComponent,
initialInteraction,
prevComponents,
prevInitialComponent,
prevInitialInteraction,
setDefaultState,
user,
isDialogOpen,
prevIsDialogOpen
]);
const getAvailableSinks = () => {
let type = or(interaction.settings.result, pond.InteractionResult.create()).type;
return components.filter(component => hasInteractionResultType(component.settings.type, type));
};
const close = () => {
closeDialogCallback();
if (mode === "add") {
setDefaultState();
}
};
const submit = () => {
let settings = interaction.settings;
if (multiNodeSource() && (settings.nodeOne > 0 || settings.nodeTwo > 0)) {
if (subtypeDropdown === 3 || subtypeDropdown === 2) {
settings.subtype = interaction.subtypeFromNodes(settings.nodeOne, settings.nodeTwo);
if (
interaction.settings.nodeOne > interaction.settings.nodeTwo &&
interaction.settings.nodeTwo !== 0
) {
//flip operator and send negative comparitor to save
settings.conditions.forEach(condition => {
if (
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
) {
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
} else if (
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN
) {
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
}
condition.value = condition.value * -1;
});
}
} else if (subtypeDropdown === 4) {
settings.subtype = Interaction.upToSubtype(settings.nodeOne);
}
} else {
settings.subtype = subtypeDropdown;
}
let sync = true;
if (interaction)
Object.keys(interaction?.settings).forEach(key => {
// Check if the key value has changed
let k = key as keyof pond.InteractionSettings;
let initial = JSON.stringify(initialInteraction?.settings[k]);
let after = JSON.stringify(interaction.settings[k]);
if (initial !== after) {
if (key !== "sortPriority") {
sync = false;
}
}
});
switch (mode) {
case "add":
interactionsAPI
.addInteraction(Number(device.settings.deviceId), settings, as)
.then(() => {
success("Interaction was successfully created");
refreshCallback();
})
.catch(() => error("Error occured when creating an interaction"))
.finally(() => close());
break;
default:
if (sync) {
let s = pond.InteractionPondSettings.create();
s.sortPriority = interaction.settings.sortPriority;
interactionsAPI
.updateInteractionPondSettings(Number(device.settings.deviceId), interaction.key(), s, as)
.then(() => {
success("Interaction pond settings were successfully updated");
refreshCallback();
})
.catch(() => error("Error when updating interaction pond settings"))
.finally(() => close());
} else {
interactionsAPI
.updateInteraction(Number(device.settings.deviceId), settings, as)
.then(() => {
success("Interaction was successfully updated");
refreshCallback();
})
.catch(() => error("Error occured when updating an interaction"))
.finally(() => close());
}
break;
}
};
const openRemoveInteraction = () => {
setIsRemoveInteractionOpen(true);
};
const closeRemoveInteraction = () => {
setIsRemoveInteractionOpen(false);
close();
};
const interactionRemoved = () => {
refreshCallback();
};
const addCondition = () => {
let updatedInteraction = Interaction.clone(interaction);
let updatedRawConditionValues = rawConditionValues;
if (numConditions < device.maxConditions() && interaction.settings.source) {
let condition = pond.InteractionCondition.create();
condition.measurementType = getMeasurements(
interaction.settings.source.type
)[0].measurementType;
condition.comparison =
condition.measurementType === Measurement.boolean ? Operator.equals : Operator.less;
updatedRawConditionValues.push("");
updatedInteraction.settings.conditions.push(condition);
setInteraction(updatedInteraction);
setNumConditions(numConditions + 1);
setRawConditionValues(updatedRawConditionValues);
}
};
const removeCondition = (index: number) => {
let updatedInteraction = Interaction.clone(interaction);
let updatedRawConditionValues = rawConditionValues;
if (numConditions > 1 && index < numConditions) {
updatedInteraction.settings.conditions.splice(index, 1);
updatedRawConditionValues.splice(index, 1);
setInteraction(updatedInteraction);
setNumConditions(numConditions - 1);
setRawConditionValues(updatedRawConditionValues);
}
};
const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
return getMeasurements(type).map(m => m.measurementType);
};
//changing the source also resets the conditions
const changeSource = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let changed = event.target.value !== componentIDToString(interaction.settings.source);
if (!changed) return;
updatedInteraction.settings.source = stringToComponentId(event.target.value.toString());
updatedInteraction.settings.conditions = initialConditions(
or(interaction.settings.source, quack.ComponentID.create()).type
);
let result = pond.InteractionResult.create(or(interaction.settings.result, undefined));
if (result.type === Result.report) {
updatedInteraction.settings.sink = emptyComponentId();
}
updatedInteraction.settings.result = result;
setInteraction(updatedInteraction);
setNumConditions(1);
setRawConditionValues([""]);
};
const changePriority = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
updatedInteraction.settings.priority = Number(event.target.value);
setInteraction(updatedInteraction);
};
const changeSortPriority = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
updatedInteraction.settings.sortPriority = Number(event.target.value);
setInteraction(updatedInteraction);
};
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
const source: Component = or(
mappedComponents.get(componentIDToString(interaction.settings.source)),
Component.create()
);
return describeMeasurement(measurementType, source.settings.type, source.settings.subtype, undefined, user);
};
const describeSink = (): MeasurementDescriber => {
const sink = mappedComponents.get(componentIDToString(interaction.settings.sink));
return describeMeasurement(
Measurement.boolean,
or(sink, Component.create()).settings.type,
or(sink, Component.create()).settings.subtype,
undefined,
user
);
};
const changeMeasurementType = (conditionIndex: number, event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let measurementType = interaction.settings.conditions[conditionIndex].measurementType;
let rawValue = rawConditionValues[conditionIndex] as any;
updatedInteraction.settings.conditions[conditionIndex].value = describeSource(
measurementType
).toStored(rawValue);
updatedInteraction.settings.conditions[conditionIndex].measurementType = event.target.value;
if (event.target.value === Measurement.boolean) {
updatedInteraction.settings.conditions[conditionIndex].comparison = Operator.equals;
}
setInteraction(updatedInteraction);
};
const changeComparison = (conditionIndex: number, event: any) => {
let updatedInteraction = Interaction.clone(interaction);
updatedInteraction.settings.conditions[conditionIndex].comparison = event.target.value;
setInteraction(updatedInteraction);
};
const changeValue = (conditionIndex: number, event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let updatedRawConditionValues = rawConditionValues;
let measurementType = interaction.settings.conditions[conditionIndex].measurementType;
updatedRawConditionValues[conditionIndex] = event.target.value;
updatedInteraction.settings.conditions[conditionIndex].value = describeSource(
measurementType
).toStored(event.target.value);
setInteraction(updatedInteraction);
setRawConditionValues(updatedRawConditionValues);
};
const changeResultValue = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let value = event.target.value;
if (event.target.type === "checkbox") {
value = event.target.checked ? 1 : 0;
}
(updatedInteraction.settings.result as pond.InteractionResult).value = isNaN(value)
? 0
: Number(value);
setInteraction(updatedInteraction);
setRawResultValue(value);
};
const changeSubtype = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let value = event.target.value;
if (!isNaN(value)) {
if (value < 2) {
updatedInteraction.settings.subtype = value;
}
setSubtypeDropdown(value);
updatedInteraction.settings.nodeOne = 0;
updatedInteraction.settings.nodeTwo = 0;
}
setInteraction(updatedInteraction);
};
const changeNode = (event: any, node: number) => {
let updatedInteraction = Interaction.clone(interaction);
let value = event.target.value;
if (!isNaN(value)) {
if (node === 1) {
updatedInteraction.settings.nodeOne = value;
} else if (node === 2) {
updatedInteraction.settings.nodeTwo = value;
}
}
setInteraction(updatedInteraction);
};
const changeResultType = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let result = interaction.settings.result as pond.InteractionResult;
result.type = event.target.value;
if (result.type === Result.report) {
updatedInteraction.settings.sink = null;
} else if (result.type === Result.toggle) {
let availableSinks = getAvailableSinks();
if (availableSinks.length > 0) {
let firstSink = availableSinks[0];
updatedInteraction.settings.sink = quack.ComponentID.fromObject({
type: firstSink.settings.type,
addressType: firstSink.settings.addressType,
address: firstSink.settings.address
});
}
}
updatedInteraction.settings.result = result;
setInteraction(updatedInteraction);
};
const changeResultMode = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let result = interaction.settings.result as pond.InteractionResult;
result.mode = event.target.value;
updatedInteraction.settings.result = result;
setInteraction(updatedInteraction);
};
const changeSink = (event: any) => {
let updatedInteraction = Interaction.clone(interaction);
updatedInteraction.settings.sink = stringToComponentId(event.target.value.toString());
if (
updatedInteraction.settings.sink.type === quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION
) {
if (
updatedInteraction.settings.sink.addressType ===
quack.AddressType.ADDRESS_TYPE_INTERNAL_SLEEP
) {
if (updatedInteraction.settings.result) {
updatedInteraction.settings.result.value = 1;
}
}
}
setInteraction(updatedInteraction);
};
const toggleReport = (_event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let notifications = or(
interaction.settings.notifications,
pond.InteractionNotifications.create()
);
notifications.reports = !notifications.reports;
updatedInteraction.settings.notifications = notifications;
setInteraction(updatedInteraction);
};
const toggleNotify = (_event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let notifications = or(
updatedInteraction.settings.notifications,
pond.InteractionNotifications.create()
);
notifications.notify = !notifications.notify;
updatedInteraction.settings.notifications = notifications;
setInteraction(updatedInteraction);
};
const toggleDaySelected = (day: string, event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let schedule = or(updatedInteraction.settings.schedule, pond.InteractionSchedule.create());
if (event.target.checked) {
if (!schedule.weekdays.includes(day)) {
schedule.weekdays.push(day);
}
} else {
schedule.weekdays.splice(schedule.weekdays.indexOf(day), 1);
}
updatedInteraction.settings.schedule = schedule;
setInteraction(updatedInteraction);
};
const setScheduleTime = (id: string, event: any) => {
let updatedInteraction = Interaction.clone(interaction);
let schedule = or(updatedInteraction.settings.schedule, pond.InteractionSchedule.create());
if (id === "shortcut") {
let value = event.target.value;
if (value === "allDay") {
schedule.timeOfDayStart = "00:00";
schedule.timeOfDayEnd = "24:00";
} else if (value === "before") {
schedule.timeOfDayStart = "00:00";
if (!schedule.timeOfDayEnd || schedule.timeOfDayEnd === "24:00") {
schedule.timeOfDayEnd = "23:59";
}
} else if (value === "after") {
schedule.timeOfDayEnd = "24:00";
if (schedule.timeOfDayStart === "00:00") {
schedule.timeOfDayStart = "00:01";
}
} else {
if (schedule.timeOfDayStart === "00:00") {
schedule.timeOfDayStart = "00:01";
}
if (schedule.timeOfDayEnd === "24:00") {
schedule.timeOfDayEnd = "23:59";
}
}
} else {
let timeOfDay =
event
.hour()
.toString()
.padStart(2, "0") +
":" +
event
.minute()
.toString()
.padStart(2, "0");
if (id === "start") {
schedule.timeOfDayStart = timeOfDay;
} else if (id === "end") {
if (timeOfDay === "00:00") {
timeOfDay = "24:00";
}
schedule.timeOfDayEnd = timeOfDay;
}
}
schedule.timezone = or(timezone, moment.tz.guess());
updatedInteraction.settings.schedule = schedule;
setInteraction(updatedInteraction);
};
const isInitialComponentTheSource = () => {
if (!interaction || !initialComponent) return false;
return sameComponentID(interaction.settings.source, initialComponent.location());
};
const isInitialComponentTheSink = () => {
if (!interaction || !initialComponent) return false;
let sinkID = JSON.stringify(quack.ComponentID.create(or(interaction.settings.sink, undefined)));
let component = initialComponent;
let componentID = JSON.stringify(
quack.ComponentID.create({
type: component.settings.type,
addressType: component.settings.addressType,
address: component.settings.address
})
);
return sinkID === componentID;
};
const invalidConditionValue = (index: number) => {
const conditions = interaction.settings.conditions;
if (conditions.length <= index) return false;
if (conditions[index].measurementType === Measurement.boolean) {
return false;
}
let value = rawConditionValues[index];
return value === "" || isNaN(Number(value));
};
const invalidConditionValues = () => {
let invalid = false;
interaction.settings.conditions.forEach((_, i) => {
if (invalidConditionValue(i)) {
invalid = true;
}
});
return invalid;
};
const invalidResultValue = () => {
return isNaN(Number(rawResultValue));
};
const getNodeOptions = (describer?: MeasurementDescriber) => {
let options = [];
let numNodes = 0;
if (initialComponent) {
if (initialComponent.lastMeasurement[0] && initialComponent.lastMeasurement[0].values[0]) {
numNodes = initialComponent.lastMeasurement[0].values[0].values.length; //use the new structure last measurement to determine the number of nodes
} else {
numNodes = GetNumNodes(
initialComponent.type(),
initialComponent.status.lastMeasurement?.measurement
);
}
}
for (let i = 0; i <= numNodes; i++) {
let label = i === 0 ? "None" : "Node " + i;
if (describer && i > 0) {
let details = describer.nodeDetails();
if (details && details.labels[i - 1]) {
label = details.labels[i - 1];
}
}
options.push(
<MenuItem key={i} value={i}>
{label}
</MenuItem>
);
}
return options;
};
//determine if the source component has nodes
const multiNodeSource = () => {
return getNodeOptions().length > 1;
};
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
const title = () => {
return (
<React.Fragment>
<Grid container justifyContent="space-between">
<Grid >
{mode && mode === "add" ? "Add Interaction" : "Interaction Settings"}
{/* <Typography variant="body2" color="textSecondary">
{sensor ? "Subnode: " + sensor.name() : device.name()}
</Typography> */}
</Grid>
</Grid>
</React.Fragment>
);
};
const sourceInput = () => {
return (
<TextField
select
id="source"
label="Source"
value={componentIDToString(interaction.settings.source)}
onChange={changeSource}
fullWidth
autoFocus={false}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}
disabled={isInitialComponentTheSource() || !canEdit || mode === "update"}
SelectProps={{ className: classes.singleInput }}>
{sourceMenuItems()}
</TextField>
);
};
const priorityInput = () => {
return (
<TextField
select
id="priority"
label="Priority"
value={interaction.settings.priority}
onChange={changePriority}
fullWidth
autoFocus={false}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
SelectProps={{ className: classes.singleInput }}>
{priorityMenuItems()}
</TextField>
);
};
const sortingInput = () => {
return (
<TextField
select
id="sorting"
label="Sorting Priority"
value={interaction.settings.sortPriority}
onChange={changeSortPriority}
fullWidth
autoFocus={false}
margin="normal"
variant="outlined"
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
SelectProps={{ className: classes.singleInput }}>
{sortingMenuItems()}
</TextField>
);
};
const measurementTypeMenuItems = () => {
const type = or(interaction.settings.source, quack.ComponentID.create()).type;
return availableMeasurementTypes(type).map(measurementType => (
<MenuItem key={measurementType} value={measurementType}>
{describeSource(measurementType).label()}
</MenuItem>
));
};
const sourceMenuItems = () => {
return availableSources().map(source => (
<MenuItem key={source.locationString()} value={source.locationString()}>
{source.name()}
</MenuItem>
));
};
const sinkMenuItems = () => {
let sinks = getAvailableSinks().map(sink => (
<MenuItem key={sink.locationString()} value={sink.locationString()}>
{sink.name()}
</MenuItem>
));
if (isResultType(Result.set)) {
sinks.push(
<MenuItem key="30-16-1" value="30-16-1">
Stay Awake
</MenuItem>
);
}
return sinks;
};
const priorityMenuItems = () => {
return ["Low", "Medium", "High"].map((name: string, i: number) => (
<MenuItem key={i} value={i}>
{name}
</MenuItem>
));
};
const sortingMenuItems = () => {
return ["None", "1", "2", "3", "4", "5"].map((name: string, i: number) => (
<MenuItem key={i} value={i}>
{name}
</MenuItem>
));
};
const isBooleanValue = (index: number) => {
if (interaction.settings.conditions.length <= index) return false;
return interaction.settings.conditions[index].measurementType === Measurement.boolean;
};
const conditionValue = (index: number, interaction: Interaction, values: string[]) => {
let condition = interaction.settings.conditions[index];
return isBooleanValue(index) ? condition.value.toString() : values[index];
};
const conditionGroup = (index: number) => {
if (index >= numConditions) return;
let measurementType = interaction.settings.conditions[index].measurementType;
let source = describeSource(measurementType, );
let isBoolean = isBooleanValue(index);
return (
<React.Fragment key={index}>
<Grid
key={"interaction"}
container
direction="row"
justifyContent="center"
alignItems="center"
spacing={2}>
{index > 0 && (
<Grid size={{ xs: 12 }}>
<Typography align="center">AND</Typography>
</Grid>
)}
<Grid 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={interaction.settings.conditions[index].measurementType}
onChange={event => changeMeasurementType(index, event)}
autoFocus={false}
margin="dense"
variant="standard"
fullWidth
InputLabelProps={{ shrink: true }}>
{measurementTypeMenuItems()}
</TextField>
</Grid>
<Grid size={{ xs: 10, sm: 3 }}>
<TextField
select
id="comparison"
required={true}
label=""
helperText="Comparator"
disabled={!isSourceValid(interaction.settings.source) || isBoolean || !canEdit}
value={interaction.settings.conditions[index].comparison}
onChange={event => changeComparison(index, event)}
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>
</Grid>
<Grid size={{ xs: 8, sm: 4 }}>
<TextField
select={isBoolean}
id="value"
required={true}
type={"text"}
label=""
helperText="Value"
error={invalidConditionValue(index)}
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
value={conditionValue(index, interaction, rawConditionValues)}
onChange={event => changeValue(index, event)}
autoFocus={false}
margin="dense"
variant="standard"
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">{source.unit()}</InputAdornment>
}}
InputLabelProps={{ shrink: true }}>
{isBoolean && [
<MenuItem key={0} value={0}>
{source.enumerations()[0]}
</MenuItem>,
<MenuItem key={1} value={1}>
{source.enumerations()[1]}
</MenuItem>
]}
</TextField>
</Grid>
<Grid size={{ xs: 2, sm: 1 }}>
<Tooltip title="Remove this condition">
<IconButton
color="default"
disabled={numConditions === 1 ||!canEdit}
aria-label="Remove condition"
onClick={() => removeCondition(index)}
className={classNames(classes.redButton, classes.noPadding)}>
<RemoveIcon />
</IconButton>
</Tooltip>
</Grid>
</Grid>
{multiNodeSource() && !sensor && (
<Grid
key={"nodes"}
container
direction="row"
justifyContent="center"
alignItems="center"
spacing={2}>
<Grid size={{ xs: 10, sm: 4 }}>
<TextField
select
id="subtype"
label="Subtype"
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
value={subtypeDropdown}
onChange={event => changeSubtype(event)}
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>
</Grid>
<Grid 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={interaction.settings.nodeOne}
onChange={event => changeNode(event, 1)}
margin="dense"
variant="standard"
fullWidth
InputLabelProps={{ shrink: true }}>
{getNodeOptions(source)}
</TextField>
)}
</Grid>
<Grid size={{ xs: 10, sm: 4 }}>
{subtypeDropdown === 3 && (
<TextField
select
id="interactionType"
label="Node 2"
disabled={!isSourceValid(interaction.settings.source) || !canEdit}
value={interaction.settings.nodeTwo}
onChange={event => changeNode(event, 2)}
margin="dense"
variant="standard"
fullWidth
InputLabelProps={{ shrink: true }}>
{getNodeOptions(source)}
</TextField>
)}
</Grid>
</Grid>
)}
</React.Fragment>
);
};
const conditionInput = () => {
const conditionGroups = [];
for (let i = 0; i < numConditions; i++) {
conditionGroups[i] = conditionGroup(i);
}
return (
<FormControl
component={"fieldset" as "div"}
className={classes.borderedContainer}
fullWidth
disabled={!isSourceValid(interaction.settings.source) || !canEdit}>
<FormLabel component={"legend" as "caption"}>Conditions</FormLabel>
{isSourceValid(interaction.settings.source) ? (
<React.Fragment>{conditionGroups}</React.Fragment>
) : (
<FormHelperText>You must select a source before adding conditions</FormHelperText>
)}
<Tooltip title="Add another condition">
<IconButton
color="primary"
disabled={numConditions >= device.maxConditions() || !canEdit}
aria-label="Add another condition"
onClick={addCondition}
className={classNames(classes.greenButton, classes.noPadding)}>
<AddIcon />
</IconButton>
</Tooltip>
</FormControl>
);
};
const isResultType = (type: quack.InteractionResultType): boolean => {
return or(interaction.settings.result, pond.InteractionResult.create()).type === type;
};
const reportingEnabled = () => {
return (
or(interaction.settings.notifications, pond.InteractionNotifications.create()).reports ||
or(interaction.settings.result, pond.InteractionResult.create()).type === Result.report
);
};
const toggleDutyCycle = (event: any) => {
let updatedInteraction = interaction;
if (!interaction.settings.result) {
updatedInteraction.settings.result = pond.InteractionResult.create();
}
if (!event.target.checked) {
if (!updatedInteraction.settings.result) {
updatedInteraction.settings.result = pond.InteractionResult.create({
dutyCycle: 0
});
} else {
updatedInteraction.settings.result.dutyCycle = 0;
}
}
setDutyCycleEnabled(event.target.checked);
setInteraction(updatedInteraction);
};
const changeDutyCycle = (event: any) => {
let dutyCycle = Number(event.target.value);
let updatedInteraction = interaction;
if (!isNaN(dutyCycle)) {
if (!updatedInteraction.settings.result) {
updatedInteraction.settings.result = pond.InteractionResult.create();
} else {
updatedInteraction.settings.result.dutyCycle = dutyCycle;
}
}
setDutyCycle(event.target.value);
setInteraction(updatedInteraction);
};
const dutyCycleInput = () => {
return (
<React.Fragment>
<Grid size={{ xs: 4, sm: 4 }}>
<FormControlLabel
control={
<Checkbox
id="enableDutyCycle"
checked={dutyCycleEnabled}
onChange={toggleDutyCycle}
value="enableDutyCycle"
color="secondary"
/>
}
label="Once every"
disabled={!canEdit}
/>
</Grid>
<Grid size={{ xs: 8, sm: 8 }}>
<TextField
id="dutyCycle"
value={dutyCycle}
onChange={changeDutyCycle}
fullWidth
autoFocus={false}
margin="dense"
variant="standard"
disabled={!canEdit || !dutyCycleEnabled}
error={isNaN(Number(dutyCycle)) || Number(dutyCycle) < 0}
InputLabelProps={{
shrink: true
}}
InputProps={{
endAdornment: <InputAdornment position="end">seconds</InputAdornment>
}}></TextField>
</Grid>
</React.Fragment>
);
};
const notificationInput = () => {
const report = reportingEnabled();
const notify = or(interaction.settings.notifications, pond.InteractionNotifications.create())
.notify;
return (
<React.Fragment>
{!isResultType(Result.report) && (
<Grid size={{xs:6, sm:3}}>
<FormControlLabel
control={
<Checkbox
id="notificationReports"
checked={reportingEnabled()}
onChange={toggleReport}
value="notificationReports"
color="secondary"
/>
}
label="Report"
disabled={!canEdit}
/>
</Grid>
)}
<Grid size={{xs: isResultType(Result.toggle) ? 6 : 12, sm: 3}} >
<FormControlLabel
control={
<Checkbox
id="interactionNotification"
checked={notify}
onChange={toggleNotify}
value="interactionNotification"
color="secondary"
/>
}
label="Notify"
disabled={!canEdit || !report}
/>
</Grid>
{notify && (
<Grid 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>
</Grid>
)}
</React.Fragment>
);
};
const sinkSelector = () => {
return (
<TextField
select
id="sink"
label=""
value={componentIDToString(interaction.settings.sink)}
onChange={changeSink}
fullWidth
autoFocus={false}
margin="dense"
variant="standard"
InputLabelProps={{ shrink: true }}
disabled={isInitialComponentTheSink() || !canEdit || mode === "update"}>
{sinkMenuItems()}
</TextField>
);
};
const setMenuItems = () => {
let type = or(interaction.settings.sink, quack.ComponentID.create()).type;
let states = extension(type).states.map((state, i) => (
<MenuItem key={state} value={i}>
{state}
</MenuItem>
));
return states;
};
const resultInput = () => {
const sink = or(interaction.settings.sink, quack.ComponentID.create());
const result = or(
interaction.settings.result,
pond.InteractionResult.create({
type: Result.report
})
);
const isRun = result.type === Result.run;
const isMotor = sink.type === ComponentType.stepperMotor;
return (
<FormControl component={"fieldset" as "div"} className={classes.borderedContainer} fullWidth>
<FormLabel component={"legend" as "caption"}>Result</FormLabel>
<Grid container direction="row" spacing={2}>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
select
id="resultType"
required={true}
label=""
value={result.type}
onChange={changeResultType}
autoFocus={false}
margin="dense"
variant="standard"
fullWidth
InputLabelProps={{ shrink: true }}
disabled={!canEdit}>
<MenuItem value={Result.report}>Report</MenuItem>
<MenuItem value={Result.set}>Set</MenuItem>
<MenuItem value={Result.toggle}>Toggle</MenuItem>
<MenuItem value={Result.run}>Run</MenuItem>
</TextField>
</Grid>
{isRun && (
<React.Fragment>
<Grid size={{ xs: 12, sm: isMotor ? 8 : 4 }}>
{sinkSelector()}
</Grid>
{isMotor && (
<Grid size={{ xs: 12, sm: 5 }} container justifyContent="center">
<TextField
select
id="mode"
label=""
value={result.mode}
onChange={changeResultMode}
fullWidth
autoFocus={false}
margin="dense"
variant="standard"
InputLabelProps={{ shrink: true }}
disabled={!canEdit}>
<MenuItem key={"Clockwise"} value={0}>
Clockwise
</MenuItem>
<MenuItem key={"Counter Clockwise"} value={1}>
Counter Clockwise
</MenuItem>
</TextField>
</Grid>
)}
<Grid size={{ xs: 2, sm: 1 }}>
<Typography variant="subtitle1" color="textSecondary" className={classes.forPrompt}>
for
</Typography>
</Grid>
<Grid size={{ xs: 10, sm: isMotor ? 6 : 4 }} container justifyContent="center">
<TextField
id="runDuration"
required={true}
label=""
helperText=""
error={invalidResultValue()}
value={rawResultValue}
onChange={changeResultValue}
autoFocus={false}
margin="dense"
variant="standard"
fullWidth
InputProps={{
endAdornment: <InputAdornment position="end">sec</InputAdornment>
}}
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
/>
</Grid>
</React.Fragment>
)}
{isResultType(Result.set) && (
<React.Fragment>
<Grid size={{ xs: 12, sm: 4 }}>
{sinkSelector()}
</Grid>
{interaction.settings.sink?.type !==
quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
<Grid size={{ xs: 2, sm: 1 }}>
<Typography
variant="subtitle1"
color="textSecondary"
className={classes.forPrompt}>
to
</Typography>
</Grid>
)}
{interaction.settings.sink?.type !==
quack.ComponentType.COMPONENT_TYPE_INTERNAL_FUNCTION && (
<Grid size={{ xs: 10, sm: 3 }} container justifyContent="center">
<TextField
select
id="resultValue"
label=""
value={result.value}
onChange={changeResultValue}
fullWidth
autoFocus={false}
margin="dense"
variant="standard"
InputLabelProps={{ shrink: true }}
disabled={!canEdit}>
{setMenuItems()}
</TextField>
</Grid>
)}
</React.Fragment>
)}
{isResultType(Result.toggle) && (
<React.Fragment>
<Grid size={{ xs: 12, sm: 5 }}>
{sinkSelector()}
</Grid>
<Grid size={{ xs: 12, sm: 3 }} container justifyContent="center">
<FormControlLabel
control={
<Switch
id="resultValue"
checked={result.value === 1}
onChange={changeResultValue}
value="toggleResultValue"
color="secondary"
/>
}
label={describeSink().enumerations()[result.value]}
disabled={!canEdit}
/>
</Grid>
</React.Fragment>
)}
{hasDeviceFeature(device.settings.upgradeChannel, "better-controls") &&
isRun &&
dutyCycleInput()}
{notificationInput()}
</Grid>
</FormControl>
);
};
const daySelector = (day: string, size: any) => {
const schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
return (
<Grid container size={{ xs: size, sm: 1 }} justifyContent="center" 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"
disabled={!canEdit}
/>
</Grid>
);
};
const scheduleInput = () => {
let schedule = or(interaction.settings.schedule, pond.InteractionSchedule.create());
let timezone = coalesce(schedule.timezone, moment.tz.guess());
let todStart = coalesce(schedule.timeOfDayStart, "00:00").split(":");
let todEnd = coalesce(schedule.timeOfDayEnd, "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
disabled={!canEdit}>
<FormLabel component={"legend" as "caption"}>Schedule</FormLabel>
<Grid container direction="row" spacing={0} justifyContent="flex-start">
{daySelector("sunday", 3)}
{daySelector("monday", 3)}
{daySelector("tuesday", 3)}
{daySelector("wednesday", 3)}
{daySelector("thursday", 4)}
{daySelector("friday", 4)}
{daySelector("saturday", 4)}
</Grid>
<Grid container direction="row" spacing={2} alignItems="center">
<Grid 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>
</Grid>
{showStart && (
<Grid size={{ xs: 5, sm: 3 }}>
<LocalizationProvider dateAdapter={AdapterMoment}>
<TimePicker
value={start}
onChange={(event) => setScheduleTime("start", event)}
slotProps={{
textField: (props) => <TextField {...props} helperText="" />,
}}
/>
</LocalizationProvider>
</Grid>
)}
{showBoth && (
<Grid size={{ xs: 2, sm: 1 }}>
<Typography
variant="subtitle1"
color="textSecondary"
className={classes.timeRange}
align="right">
to
</Typography>
</Grid>
)}
{showEnd && (
<Grid size={{ xs: 5, sm: 3 }}>
<LocalizationProvider dateAdapter={AdapterMoment}>
<TimePicker
value={end}
onChange={(event: any) => setScheduleTime("end", event)}
slotProps={{
textField: (props) => <TextField {...props} helperText="" />,
}}
/>
</LocalizationProvider>
</Grid>
)}
</Grid>
</FormControl>
);
};
const content = () => {
return (
<Grid container direction="row" spacing={2}>
<Grid size={{ xs: 12 }}>
{sourceInput()}
</Grid>
{hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && (
<Grid size={{ xs: 12 }}>
<div style={{ display: "flex", flexDirection: "row" }}>
{priorityInput()}
<div style={{ marginLeft: theme.spacing(2) }}></div>
{sortingInput()}
</div>
</Grid>
)}
<Grid size={{ xs: 12 }}>
{conditionInput()}
</Grid>
<Grid size={{ xs: 12 }}>
{resultInput()}
</Grid>
<Grid size={{ xs: 12 }} >
{scheduleInput()}
</Grid>
</Grid>
);
};
const actions = () => {
return (
<Grid container justifyContent="space-between" direction="row" width={"100%"}>
<Grid size={{ xs: 5 }}>
{mode === "update" && canEdit && (
<DeleteButton onClick={openRemoveInteraction}>Delete</DeleteButton>
)}
</Grid>
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
{/* <Button onClick={close} color="primary">
Cancel
</Button>
{canEdit && (
<Button
onClick={submit}
color="primary"
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
Submit
</Button>
)} */}
</Grid>
</Grid>
);
};
return (
<ResponsiveDialog
fullWidth
open={isDialogOpen}
onClose={close}
aria-labelledby="interaction-settings-dialog">
<DialogTitle id="interaction-settings-title">{title()}</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>{content()}</DialogContent>
<Divider />
<DialogActions>{actions()}</DialogActions>
{mode === "update" && (
<RemoveInteraction
device={device}
interaction={interaction}
isDialogOpen={isRemoveInteractionOpen}
closeDialogCallback={closeRemoveInteraction}
refreshCallback={interactionRemoved}
/>
)}
</ResponsiveDialog>
);
}