finished the form for creating an interaction/alert for a device through an object

This commit is contained in:
csawatzky 2025-11-28 16:36:59 -06:00
parent ce7c90f384
commit bb92077d0b
6 changed files with 916 additions and 1005 deletions

View file

@ -1,973 +0,0 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Card,
Checkbox,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
FormControlLabel,
FormHelperText,
FormLabel,
Grid2 as Grid,
IconButton,
InputAdornment,
List,
ListItem,
ListItemIcon,
ListItemText,
ListSubheader,
MenuItem,
Select,
TextField,
Theme,
Tooltip,
Typography
} from "@mui/material";
import { ExpandMore, RemoveCircle as RemoveIcon, AddCircle as AddIcon } from "@mui/icons-material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Option } from "common/SearchSelect";
import classNames from "classnames";
import { Component, Interaction } from "models";
import moment from "moment";
import { getFriendlyName, getMeasurements } from "pbHelpers/ComponentType";
import { Measurement, Operator } from "pbHelpers/Enums";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
import { useNotificationAPI } from "providers/pond/notificationAPI";
import React, { useEffect, useState } from "react";
import { capitalize, cloneDeep } from "lodash";
import { timeOfDayDescriptor } from "pbHelpers/Interaction";
import { TimePicker } from "@mui/x-date-pickers";
import { getThemeType } from "theme/themeType";
import { green, red } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
interface Props {
linkedComponents: Map<string, Component>;
componentDevices: Map<string, number>;
objectType: pond.ObjectType;
objectKey: string;
}
interface Alert {
conditions: pond.InteractionCondition[];
components: Component[];
}
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
},
timeRange: {
fontSize: "0.875em",
marginTop: "20px"
},
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 5
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 5
}
});
});
export default function ObjectAlerts(props: Props) {
const { linkedComponents, componentDevices, objectKey, objectType } = props;
const classes = useStyles();
const { openSnack } = useSnackbar();
const [{ as }] = useGlobalState();
const interactionsAPI = useInteractionsAPI();
const [interactions, setInteractions] = useState<Interaction[]>([]);
const [typeOptions, setTypeOptions] = useState<Option[]>([]);
const [alerts, setAlerts] = useState<Alert[]>([]);
//the interaction key to the component it is set on
const [interactionComponent, setInteractionComponent] = useState<Map<string, string>>(new Map());
//state variables for notifications
const notificationAPI = useNotificationAPI();
const [recentNotifications, setRecentNotifications] = useState<pond.Notification[]>([]);
const [notificationsLoading, setNotificationsLoading] = useState(false);
const [totalNotifications, setTotalNotifications] = useState(0);
//stat variables for adding new alerts
const [newAlertOpen, setNewAlertOpen] = useState(false);
const [selectedAlertComponents, setSelectedAlertComponents] = useState<string[]>([]);
const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>(
quack.ComponentType.COMPONENT_TYPE_INVALID
);
const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]);
const [nodeOptions, setNodeOptions] = useState<JSX.Element[]>([]);
const [subtypeDropdown, setSubtypeDropdown] = useState<number>(0);
//condition value strings - so that decimals are easier to type into the field
const [valStrings, setValStrings] = useState(["0", "0"]);
//the nodes for the subnode interactions
const [nodeOne, setNodeOne] = useState(0);
const [nodeTwo, setNodeTwo] = useState(0);
//schedule variables
const [schedule, setSchedule] = useState(
pond.InteractionSchedule.create({
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"]
})
);
useEffect(() => {
//loop through the linked components to get each of their interactions
//TODO-CS: consider making an api function to get the interactions for multiple components, possibly based off objects, in one call,
//to get all interactions for bin components this would solve the issue of making multiple calls as well as not having to use async await
let newInteractions: Interaction[] = [];
let icMap: Map<string, string> = new Map();
let includedOps: quack.ComponentType[] = [];
let typeOps: Option[] = [];
let index = 0;
//use async to make sure to wait for the response from the api call and have the interactions set before going to get the next one
//this prevents seperate responses from assigning to the array without knowing about interactions from those seperate responses
//for example: without async two responses come back at the same time, one has two interactions and another has one but, neither have put their interactions
//into the array yet so they both reference an empty array, the first response puts its two into an empty array and sets it but then the next one also puts
//its response into an empty array as well and sets it causing the first two to be lost
linkedComponents.forEach(async (comp, key) => {
index++;
//get the interactions for the component
let device = componentDevices.get(key);
if (device !== undefined) {
await interactionsAPI.listInteractionsByComponent(device, comp.location(), undefined, as).then(resp => {
if (resp.length > 0) {
resp.forEach(interaction => {
icMap.set(interaction.key(), key);
});
newInteractions = newInteractions.concat(resp);
}
});
}
//set the state variables if it is the last one in the list
if (index === linkedComponents.size) {
setInteractionComponent(icMap);
setInteractions([...newInteractions]);
}
//use the component type to the type options if it is not already present
if (!includedOps.includes(comp.type())) {
includedOps.push(comp.type());
typeOps.push({
label: getFriendlyName(comp.type()),
value: comp.type()
});
}
});
setTypeOptions(typeOps);
}, [linkedComponents, interactionsAPI, componentDevices]);
const matchConditions = (interaction: Interaction, alert: Alert) => {
//if the number of conditions are not the same they are different
if (interaction.settings.conditions.length !== alert.conditions.length) {
return false;
}
//continure with the comparison
let matching = true;
interaction.settings.conditions.forEach((condition, i) => {
if (
condition.comparison !== alert.conditions[i].comparison ||
condition.measurementType !== alert.conditions[i].measurementType ||
condition.value !== alert.conditions[i].value
) {
matching = false;
}
});
return matching;
};
//build the alerts to display
useEffect(() => {
//loop through the interactions
let currentAlerts: Alert[] = [];
interactions.forEach(interaction => {
//if the interaction sends notifications
if (interaction.settings.notifications && interaction.settings.notifications.notify) {
//determine if the interaction already has an alert in the alerts array
let similarAlert: number = -1;
currentAlerts.forEach((alert, i) => {
if (matchConditions(interaction, alert)) {
similarAlert = i;
}
});
//if no current alert matched the conditions add a new alert to the array
let compKey = interactionComponent.get(interaction.key());
if (compKey) {
let component = linkedComponents.get(compKey);
if (component) {
if (similarAlert === -1) {
let newAlert: Alert = {
conditions: interaction.settings.conditions,
components: [component]
};
currentAlerts.push(newAlert);
} else {
currentAlerts[similarAlert].components.push(component);
}
}
}
}
});
setAlerts(currentAlerts);
}, [interactions, interactionComponent, linkedComponents]);
//get the notifications for the components on an object
useEffect(() => {
if (notificationsLoading) return;
setNotificationsLoading(true);
notificationAPI
.listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
.then(resp => {
setRecentNotifications(resp.data.notifications);
setTotalNotifications(resp.data.total);
})
.catch(_err => {})
.finally(() => {
setNotificationsLoading(false);
});
}, [objectKey, objectType, notificationAPI]); //eslint-disable-line react-hooks/exhaustive-deps
//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
linkedComponents.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);
}, [linkedComponents, selectedAlertComponents]);
const loadMoreNotifications = () => {
console.log("load more");
let current = recentNotifications;
setNotificationsLoading(true);
notificationAPI
.listObjectNotifications(
objectKey,
objectType,
10,
current.length,
undefined,
undefined,
undefined,
as
)
.then(resp => {
if (resp.data.notifications.length > 0) {
let c = current.concat(resp.data.notifications);
setRecentNotifications([...c]);
}
})
.catch(_err => {})
.finally(() => {
setNotificationsLoading(false);
});
};
const readableCondition = (condition: pond.InteractionCondition) => {
let describer = describeMeasurement(condition.measurementType);
let type = describer.label();
let comparison =
condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_EQUAL_TO
? "Exactly"
: condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN
? "Above"
: "Below";
let value = describer.toDisplay(condition.value);
return (
<Box>
<Typography>{type + " " + comparison + " " + value + describer.GetUnit()}</Typography>
</Box>
);
};
const alertAccordion = (alert: Alert) => {
return (
<Accordion style={{ width: "100%" }}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Grid container direction="column">
{alert.conditions.map((condition, i) => (
<Grid key={i}>
{readableCondition(condition)}
</Grid>
))}
</Grid>
</AccordionSummary>
<AccordionDetails>
<List>
<ListSubheader>Reporting Components</ListSubheader>
{alert.components.map((comp, i) => (
<ListItem key={i}>{comp.name()}</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
);
};
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 newAlert = pond.InteractionSettings.create();
newAlert.conditions = conditions;
newAlert.instance = 1;
newAlert.schedule = schedule;
newAlert.subtype = Interaction.create().subtypeFromNodes(nodeOne, nodeTwo);
newAlert.result = pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_REPORT
});
newAlert.notifications = pond.InteractionNotifications.create({
notify: true
});
interactionsAPI
.addInteractionToComponents(compIds, newAlert, as)
.then(_resp => {
openSnack("interaction added to selected components");
})
.catch(_err => {
openSnack("there was a problem adding the interaction to the selected components");
});
setNewAlertOpen(false);
};
//display the components that match the type that was selected for the new alert
const listMatchingComponents = () => {
let matching: Component[] = [];
linkedComponents.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 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];
};
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}>
<Grid
key={"interaction"}
container
direction="row"
alignItems="center"
spacing={2}>
{index > 0 && (
<Grid size={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={condition.measurementType}
onChange={event => {
condition.measurementType = +event.target.value;
changeCondition(condition, index);
}}
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={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>
</Grid>
<Grid 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>
</Grid>
<Grid size={{xs:2, sm:1}}>
{index === 0 ? (
<Tooltip title="Add another condition">
<IconButton
color="primary"
disabled={conditions.length > 1}
aria-label="Add another condition"
onClick={() => {
let c = cloneDeep(conditions);
let newConditions = c.concat(initialConditions(newAlertComponentType));
setConditions(newConditions);
}}
className={classNames(classes.greenButton, classes.noPadding)}>
<AddIcon />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Remove this condition">
<IconButton
color="default"
//disabled={!canEdit}
aria-label="Remove condition"
onClick={() => {
let c = conditions;
c.splice(index, 1);
setConditions([...c]);
}}
className={classNames(classes.redButton, classes.noPadding)}>
<RemoveIcon />
</IconButton>
</Tooltip>
)}
</Grid>
</Grid>
{nodeOptions.length > 2 && (
<Grid
key={"nodes"}
container
direction="row"
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 => {
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>
</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={nodeOne}
onChange={event => {
setNodeOne(+event.target.value);
}}
margin="dense"
variant="standard"
fullWidth
InputLabelProps={{ shrink: true }}>
{nodeOptions}
</TextField>
)}
</Grid>
<Grid 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>
)}
</Grid>
</Grid>
)}
</React.Fragment>
);
};
const conditionInput = () => {
// const conditionGroups: JSX.Element[] = [];
// for (let i = 0; i < conditions.length; i++) {
// conditionGroups[i] = conditionGroup(i);
// }
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>
)}
</FormControl>
);
};
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 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 (
<Grid 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"
/>
</Grid>
);
};
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>
<Grid 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)}
</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}}>
<TimePicker
//renderInput={props => <TextField {...props} helperText="" />}
value={start}
onChange={(event: any) => setScheduleTime("start", event)}
/>
</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}}>
<TimePicker
//renderInput={props => <TextField {...props} helperText="" />}
value={end}
onChange={(event: any) => setScheduleTime("end", event)}
/>
</Grid>
)}
</Grid>
</FormControl>
);
};
const newAlertDialog = () => {
return (
<ResponsiveDialog
open={newAlertOpen}
onClose={() => {
setNewAlertOpen(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>
{typeOptions.map(option => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</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()}
</DialogContent>
<DialogActions>
<Button onClick={() => setNewAlertOpen(false)} variant="contained">
Cancel
</Button>
<Button
disabled={selectedAlertComponents.length === 0}
onClick={addInteractionToComponents}
variant="contained"
color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const alertDisplay = () => {
return (
<List style={{ margin: 5 }}>
<ListSubheader>Alerts</ListSubheader>
{alerts.map((alert, i) => (
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
))}
</List>
);
};
const notificationList = () => {
return (
<List style={{ margin: 5 }}>
<ListSubheader>Notifications</ListSubheader>
{recentNotifications && (
<React.Fragment>
{recentNotifications.map((notification, i) => (
<ListItem key={i} className={i % 2 === 0 ? classes.dark : classes.light}>
<Typography style={{ fontWeight: 650 }}>
{moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")}
</Typography>{" "}
:{" "}
<Typography>
{notification.settings?.title + " - " + notification.settings?.subtitle}
</Typography>
</ListItem>
))}
{recentNotifications.length < totalNotifications && (
<ListItem key={"button"}>
<Button onClick={loadMoreNotifications} disabled={notificationsLoading}>
Get More
</Button>
</ListItem>
)}
</React.Fragment>
)}
</List>
);
};
return (
<Card raised style={{ margin: 5 }}>
<Box padding={1} display="flex" justifyContent="space-between">
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Alerts & Notifications</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
setNewAlertOpen(true);
}}>
New Alert
</Button>
</Box>
{alertDisplay()}
{notificationList()}
{newAlertDialog()}
</Card>
);
}

View file

@ -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) => (

View file

@ -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)

View file

@ -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>
)

View file

@ -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,12 +205,16 @@ 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)
}}/>

View file

@ -50,11 +50,9 @@ import { GrainCable } from "models/GrainCable";
// import { Controller } from "models/Controller";
import { Pressure } from "models/Pressure";
import { CheckCircle, ExpandMore, Warning } from "@mui/icons-material";
import { getThemeType } from "theme";
import BinGraphs from "bin/graphs/BinGraphs";
import BinTour from "bin/BinTour";
import { getBinModel } from "common/DataImports/BinCables/BinCableImporter";
import ObjectAlerts from "objects/ObjectAlerts";
import DevicePresetController from "device/presets/devicePresetController";
import { DevicePreset } from "models/DevicePreset";
import BinVisualizerV2 from "bin/BinVisualizerV2";
@ -866,6 +864,7 @@ export default function Bin(props: Props) {
devices={devices}
objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/>
</Box>
)}
@ -1004,6 +1003,7 @@ export default function Bin(props: Props) {
devices={devices}
objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
permissions={permissions}
/>
</Box>
</TabPanelMine>