refactoring object interactions into multiple components and taking into account controller interactions having to be on the same device where the alerts dont matter
This commit is contained in:
parent
bdddcfc103
commit
3cdddb928f
7 changed files with 459 additions and 2 deletions
74
src/objects/objectInteractions/Alerts.tsx
Normal file
74
src/objects/objectInteractions/Alerts.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { ExpandMore } from "@mui/icons-material";
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, List, ListItem, ListSubheader, Typography } from "@mui/material";
|
||||
import { Component } from "models";
|
||||
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
|
||||
import { pond, quack } from "protobuf-ts/pond";
|
||||
import React, { useState } from "react";
|
||||
import NewObjectInteraction from "./NewObjectInteraction";
|
||||
|
||||
export interface Alert {
|
||||
conditions: pond.InteractionCondition[];
|
||||
sourceComponents: Component[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
alerts: Alert[]
|
||||
}
|
||||
|
||||
export default function Alerts(props: Props){
|
||||
const {alerts} = props
|
||||
const [openNewAlert, setOpenNewAlert] = useState(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 />}>
|
||||
<Grid2 container direction="column">
|
||||
{alert.conditions.map((condition, i) => (
|
||||
<Grid2 key={i}>
|
||||
{readableCondition(condition)}
|
||||
</Grid2>
|
||||
))}
|
||||
</Grid2>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<List>
|
||||
<ListSubheader>Reporting Components</ListSubheader>
|
||||
{alert.sourceComponents.map((comp, i) => (
|
||||
<ListItem key={i}>{comp.name()}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NewObjectInteraction open={openNewAlert} onClose={()=>{}}/>
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader>Alerts</ListSubheader>
|
||||
{alerts.map((alert, i) => (
|
||||
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
61
src/objects/objectInteractions/Controls.tsx
Normal file
61
src/objects/objectInteractions/Controls.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Accordion, AccordionDetails, AccordionSummary, List, ListItem, ListSubheader } from "@mui/material";
|
||||
import { Component, Device } from "models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export interface Control {
|
||||
conditions: pond.InteractionCondition[];
|
||||
sourceComponents: Component[];
|
||||
result: pond.InteractionResult | null;
|
||||
sinkComponent: Component
|
||||
device: Device
|
||||
}
|
||||
|
||||
interface Props {
|
||||
controls: Control[]
|
||||
}
|
||||
|
||||
export default function Controls(props: Props){
|
||||
const { controls } = props
|
||||
const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map())
|
||||
const [deviceNameMap, setDeviceNameMap] = useState<Map<number, string>>(new Map())
|
||||
|
||||
useEffect(()=>{
|
||||
let map: Map<number, Control[]> = new Map()
|
||||
let nameMap: Map<number, string> = new Map()
|
||||
controls.forEach(control => {
|
||||
let mappedControls = map.get(control.device.id())
|
||||
if(mappedControls){
|
||||
mappedControls.push(control)
|
||||
}else{
|
||||
map.set(control.device.id(), [control])
|
||||
}
|
||||
if(!nameMap.get(control.device.id())){
|
||||
nameMap.set(control.device.id(), control.device.name())
|
||||
}
|
||||
})
|
||||
setDeviceControls(map)
|
||||
setDeviceNameMap(nameMap)
|
||||
},[controls])
|
||||
|
||||
const controlAccordion = (control: Control) => {
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary>{control.device.name()}</AccordionSummary>
|
||||
<AccordionDetails></AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader>Controls</ListSubheader>
|
||||
{controls.map((control, i) => (
|
||||
<ListItem key={i}>{controlAccordion(control)}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
94
src/objects/objectInteractions/NewObjectInteraction.tsx
Normal file
94
src/objects/objectInteractions/NewObjectInteraction.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { Button, DialogActions, DialogContent, DialogTitle, MenuItem, Select } from "@mui/material";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { getMeasurements } from "pbHelpers/ComponentType";
|
||||
import { Measurement, Operator } from "pbHelpers/Enums";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
onClose: (refreshCallback: boolean) => void
|
||||
//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
|
||||
}
|
||||
|
||||
export default function NewObjectInteraction(props: Props){
|
||||
const { open, onClose } = props
|
||||
const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>(
|
||||
quack.ComponentType.COMPONENT_TYPE_INVALID
|
||||
);
|
||||
const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]);
|
||||
const [selectedAlertComponents, setSelectedAlertComponents] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {},[])
|
||||
|
||||
|
||||
const close = (refresh: boolean) => {
|
||||
onClose(refresh)
|
||||
}
|
||||
const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
|
||||
return getMeasurements(type).map(m => m.measurementType);
|
||||
};
|
||||
|
||||
const initialConditions = (type: quack.ComponentType): pond.InteractionCondition[] => {
|
||||
let measurementType = availableMeasurementTypes(type)[0];
|
||||
let condition = pond.InteractionCondition.create({
|
||||
measurementType: measurementType,
|
||||
comparison: measurementType === Measurement.boolean ? Operator.equals : Operator.less,
|
||||
value: 0
|
||||
});
|
||||
return [condition];
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
close(false);
|
||||
}}>
|
||||
<DialogTitle>Set New Alert</DialogTitle>
|
||||
<DialogContent>
|
||||
{/* Dropdown to select the component type to add the interaction to */}
|
||||
<Select
|
||||
id="componentType"
|
||||
label="Component Type"
|
||||
fullWidth
|
||||
displayEmpty
|
||||
value={newAlertComponentType}
|
||||
onChange={e => {
|
||||
setNewAlertComponentType(e.target.value as quack.ComponentType);
|
||||
setSelectedAlertComponents([]);
|
||||
setConditions(initialConditions(e.target.value as quack.ComponentType));
|
||||
}}>
|
||||
<MenuItem key={0} value={0}>
|
||||
Select Component Type
|
||||
</MenuItem>
|
||||
{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={() => close(false)} variant="contained">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selectedAlertComponents.length === 0}
|
||||
onClick={addInteractionToComponents}
|
||||
variant="contained"
|
||||
color="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
}
|
||||
0
src/objects/objectInteractions/Notifications.tsx
Normal file
0
src/objects/objectInteractions/Notifications.tsx
Normal file
204
src/objects/objectInteractions/ObjectInteractions.tsx
Normal file
204
src/objects/objectInteractions/ObjectInteractions.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { Box, Button, Card, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import Alerts, { Alert } from "./Alerts";
|
||||
import Controls, { Control } from "./Controls";
|
||||
import { Component, Device, Interaction } from "models";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import { Option } from "common/SearchSelect";
|
||||
import { useInteractionsAPI } from "hooks";
|
||||
import { useGlobalState } from "providers";
|
||||
import { extension, getFriendlyName } from "pbHelpers/ComponentType";
|
||||
import { sameComponentID } from "pbHelpers/Component";
|
||||
|
||||
interface Props {
|
||||
linkedComponents: Map<string, Component>; //the component key to the component object
|
||||
componentDevices: Map<string, number>;
|
||||
devices: Device[];
|
||||
}
|
||||
|
||||
export default function ObjectInteractions(props: Props) {
|
||||
const { linkedComponents, componentDevices, devices } = props
|
||||
const [{as}] = useGlobalState()
|
||||
//list of alerts, each alert contains a list of interactions with matching conditions
|
||||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||||
//list of interactions relating to a controller
|
||||
const [controls, setControls] = useState<Control[]>([]);
|
||||
const interactionsAPI = useInteractionsAPI();
|
||||
const [sinkOptions, setSinkOptions] = useState<Component[]>([])
|
||||
const [typeOptions, setTypeOptions] = useState<Option[]>([]);
|
||||
|
||||
|
||||
const matchConditions = (interaction: Interaction, set: Alert | Control) => {
|
||||
//if the number of conditions are not the same they are different
|
||||
if (interaction.settings.conditions.length !== set.conditions.length) {
|
||||
return false;
|
||||
}
|
||||
//continue with the comparison
|
||||
let matching = true;
|
||||
interaction.settings.conditions.forEach((condition, i) => {
|
||||
if (
|
||||
condition.comparison !== set.conditions[i].comparison ||
|
||||
condition.measurementType !== set.conditions[i].measurementType ||
|
||||
condition.value !== set.conditions[i].value
|
||||
) {
|
||||
matching = false;
|
||||
}
|
||||
});
|
||||
return matching;
|
||||
};
|
||||
|
||||
const matchResult = (interaction: Interaction, set: Control) => {
|
||||
let interactionResult = interaction.settings.result
|
||||
let setResult = set.result
|
||||
let matching = true
|
||||
// if anything in the result for the interaction is different from the set make matching false
|
||||
if(interactionResult?.type !== setResult?.type ||
|
||||
interactionResult?.mode !== setResult?.mode ||
|
||||
interactionResult?.value !== setResult?.value ||
|
||||
interactionResult?.dutyCycle !== setResult?.dutyCycle
|
||||
){
|
||||
matching = false
|
||||
}
|
||||
return matching
|
||||
}
|
||||
|
||||
const findDevice = (deviceID: number) => {
|
||||
for (let device of devices) {
|
||||
if (device.id() === deviceID) return device
|
||||
}
|
||||
}
|
||||
|
||||
const findSinkComponent = (sinks: Component[], interactionSink: quack.ComponentID, sourceDevice: number) => {
|
||||
for (let component of sinks) {
|
||||
if (sameComponentID(component.location(), interactionSink) && sourceDevice === componentDevices.get(component.key())){
|
||||
return component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const newInteractions: Interaction[] = [];
|
||||
const interactionSourceMap = new Map<string, string>();
|
||||
const includedOps: quack.ComponentType[] = [];
|
||||
const typeOps: Option[] = [];
|
||||
const sinkOptions: Component[] = [];
|
||||
const alerts: Alert[] = [];
|
||||
const controls: Control[] = [];
|
||||
|
||||
const run = async () => {
|
||||
// Run all component API fetches in parallel
|
||||
await Promise.all(
|
||||
[...linkedComponents].map(async ([key, comp]) => {
|
||||
const device = componentDevices.get(key);
|
||||
|
||||
if (device) {
|
||||
const resp = await interactionsAPI.listInteractionsByComponent(
|
||||
device,
|
||||
comp.location(),
|
||||
undefined,
|
||||
as
|
||||
);
|
||||
|
||||
resp.forEach(interaction => interactionSourceMap.set(interaction.key(), key));
|
||||
newInteractions.push(...resp);
|
||||
}
|
||||
|
||||
// Collect component types
|
||||
if (!includedOps.includes(comp.type())) {
|
||||
includedOps.push(comp.type());
|
||||
typeOps.push({
|
||||
label: getFriendlyName(comp.type()),
|
||||
value: comp.type(),
|
||||
});
|
||||
}
|
||||
|
||||
// Collect controller components
|
||||
if (extension(comp.type()).isController) {
|
||||
sinkOptions.push(comp);
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
run().then(() => {
|
||||
newInteractions.forEach(interaction => {
|
||||
//alert and control are not mutally exclusive, it is possible to be both if the control is sending notifications
|
||||
if (interaction.isAlert()) {
|
||||
// Find matching alert
|
||||
let similarAlertIndex = alerts.findIndex(alert =>
|
||||
matchConditions(interaction, alert)
|
||||
);
|
||||
|
||||
const compKey = interactionSourceMap.get(interaction.key());
|
||||
const component = compKey ? linkedComponents.get(compKey) : undefined;
|
||||
|
||||
if (component) {
|
||||
if (similarAlertIndex === -1) {
|
||||
alerts.push({
|
||||
conditions: interaction.settings.conditions,
|
||||
sourceComponents: [component],
|
||||
});
|
||||
} else {
|
||||
alerts[similarAlertIndex].sourceComponents.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (interaction.isControl()) {
|
||||
if (!interaction.settings.sink) return //if there is no sink it cant be a control so move on to the next
|
||||
const compKey = interactionSourceMap.get(interaction.key());
|
||||
if (!compKey) return //we have to have a valid component key
|
||||
const component = linkedComponents.get(compKey);
|
||||
const deviceID = componentDevices.get(compKey);
|
||||
if (!deviceID) return //we have to know which device it is for
|
||||
const controlDevice = findDevice(deviceID)
|
||||
if (!controlDevice) return //failed to get the device from the array
|
||||
const sinkComponent = findSinkComponent(sinkOptions, interaction.settings.sink, deviceID)
|
||||
if (!sinkComponent) return //failed to get the correct sink component
|
||||
|
||||
let similarControlIndex = controls.findIndex(control =>
|
||||
matchConditions(interaction, control) &&
|
||||
matchResult(interaction, control) &&
|
||||
sameComponentID(interaction.settings.sink, control.sinkComponent.location()) && //if the sink for the interaction is the same address as the existing control
|
||||
deviceID === control.device.id() //if the device id for source component is the same as the device id in the control
|
||||
);
|
||||
|
||||
if (component && interaction.settings.result) {
|
||||
if (similarControlIndex === -1) {
|
||||
controls.push({
|
||||
conditions: interaction.settings.conditions,
|
||||
result: interaction.settings.result,
|
||||
sourceComponents: [component],
|
||||
sinkComponent: sinkComponent,
|
||||
device: controlDevice
|
||||
});
|
||||
} else {
|
||||
controls[similarControlIndex].sourceComponents.push(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setTypeOptions(typeOps);
|
||||
setSinkOptions(sinkOptions);
|
||||
setAlerts(alerts);
|
||||
console.log(alerts)
|
||||
setControls(controls);
|
||||
console.log(controls)
|
||||
}).catch(err => {
|
||||
console.error("Interaction fetch error:", err)
|
||||
})
|
||||
}, [linkedComponents, interactionsAPI, componentDevices, as]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card raised style={{ margin: 5 }}>
|
||||
<Box padding={1}>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
|
||||
<Controls controls={controls}/>
|
||||
<Alerts alerts={alerts}/>
|
||||
{/* <Notifications /> */}
|
||||
</Box>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue