getting the new object interaction dialog filtering the components properly regarding devices being passed in, finished the display part for alert and notification

This commit is contained in:
csawatzky 2025-11-27 17:08:14 -06:00
parent 3cdddb928f
commit ce7c90f384
6 changed files with 314 additions and 101 deletions

View file

@ -1,10 +1,11 @@
import { ExpandMore } from "@mui/icons-material"; import { Add, ExpandMore } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, List, ListItem, ListSubheader, Typography } from "@mui/material"; import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, IconButton, List, ListItem, ListSubheader, Typography } from "@mui/material";
import { Component } from "models"; import { Component } from "models";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond, quack } from "protobuf-ts/pond"; import { pond, quack } from "protobuf-ts/pond";
import React, { useState } from "react"; import React, { useState } from "react";
import NewObjectInteraction from "./NewObjectInteraction"; import NewObjectInteraction from "./NewObjectInteraction";
import { Option } from "common/SearchSelect";
export interface Alert { export interface Alert {
conditions: pond.InteractionCondition[]; conditions: pond.InteractionCondition[];
@ -13,11 +14,12 @@ export interface Alert {
interface Props { interface Props {
alerts: Alert[] alerts: Alert[]
addNew?: () => void
} }
export default function Alerts(props: Props){ export default function Alerts(props: Props){
const {alerts} = props const {alerts, addNew} = props
const [openNewAlert, setOpenNewAlert] = useState(false) // const [openNewAlert, setOpenNewAlert] = useState(false)
const readableCondition = (condition: pond.InteractionCondition) => { const readableCondition = (condition: pond.InteractionCondition) => {
let describer = describeMeasurement(condition.measurementType); let describer = describeMeasurement(condition.measurementType);
@ -62,9 +64,19 @@ export default function Alerts(props: Props){
return ( return (
<React.Fragment> <React.Fragment>
<NewObjectInteraction open={openNewAlert} onClose={()=>{}}/> {/* <NewObjectInteraction open={openNewAlert} onClose={(refresh)=>{
setOpenNewAlert(false)
if(refresh && refreshCallback) refreshCallback()
}} typeOptions={typeOptions} linkedComponents={linkedComponents}/> */}
<List style={{ margin: 5 }}> <List style={{ margin: 5 }}>
<ListSubheader>Alerts</ListSubheader> <ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Typography>
Alerts
</Typography>
{addNew &&
<IconButton onClick={()=>{addNew()}} color="primary"><Add /></IconButton>
}
</ListSubheader>
{alerts.map((alert, i) => ( {alerts.map((alert, i) => (
<ListItem key={i}>{alertAccordion(alert)}</ListItem> <ListItem key={i}>{alertAccordion(alert)}</ListItem>
))} ))}

View file

@ -1,4 +1,5 @@
import { Accordion, AccordionDetails, AccordionSummary, List, ListItem, ListSubheader } from "@mui/material"; import { Add } from "@mui/icons-material";
import { Accordion, AccordionDetails, AccordionSummary, Divider, IconButton, List, ListItem, ListSubheader, Menu, MenuItem, Typography } from "@mui/material";
import { Component, Device } from "models"; import { Component, Device } from "models";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
@ -12,17 +13,20 @@ export interface Control {
} }
interface Props { interface Props {
devices: Device[]
controls: Control[] controls: Control[]
addNew?: (deviceID: number) => void
} }
export default function Controls(props: Props){ export default function Controls(props: Props){
const { controls } = props const { devices, controls, addNew } = props
const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map()) const [deviceControls, setDeviceControls] = useState<Map<number, Control[]>>(new Map())
const [deviceNameMap, setDeviceNameMap] = useState<Map<number, string>>(new Map()) const [openDeviceMenu, setOpenDeviceMenu] = useState(false)
const [anchor, setAnchor] = useState<null | HTMLElement>(null)
useEffect(()=>{ useEffect(()=>{
let map: Map<number, Control[]> = new Map() let map: Map<number, Control[]> = new Map()
let nameMap: Map<number, string> = new Map()
controls.forEach(control => { controls.forEach(control => {
let mappedControls = map.get(control.device.id()) let mappedControls = map.get(control.device.id())
if(mappedControls){ if(mappedControls){
@ -30,30 +34,72 @@ export default function Controls(props: Props){
}else{ }else{
map.set(control.device.id(), [control]) map.set(control.device.id(), [control])
} }
if(!nameMap.get(control.device.id())){
nameMap.set(control.device.id(), control.device.name())
}
}) })
setDeviceControls(map) setDeviceControls(map)
setDeviceNameMap(nameMap)
},[controls]) },[controls])
const controlAccordion = (control: Control) => { const controlAccordion = (control: Control) => {
return ( return (
<Accordion> <Accordion>
<AccordionSummary>{control.device.name()}</AccordionSummary> <AccordionSummary>Show what the control does</AccordionSummary>
<AccordionDetails></AccordionDetails> <AccordionDetails>show the components that are doing it</AccordionDetails>
</Accordion> </Accordion>
) )
} }
const deviceMenu = () => {
return (
<Menu
open={openDeviceMenu}
anchorEl={anchor}
onClose={() => {
setAnchor(null)
setOpenDeviceMenu(false)
}}>
{devices.map(dev => (
<MenuItem
key={dev.id()}
onClick={() => {
setAnchor(null)
setOpenDeviceMenu(false)
if (addNew) addNew(dev.id())
}}>
{dev.name()}
</MenuItem>
))}
</Menu>
)
}
return ( return (
<React.Fragment> <React.Fragment>
{deviceMenu()}
<List style={{ margin: 5 }}> <List style={{ margin: 5 }}>
<ListSubheader>Controls</ListSubheader> <ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
{controls.map((control, i) => ( <Typography>
<ListItem key={i}>{controlAccordion(control)}</ListItem> Alerts
</Typography>
<IconButton
onClick={(e)=>{
setOpenDeviceMenu(true)
setAnchor(e.currentTarget)
}}
color="primary">
<Add />
</IconButton>
</ListSubheader>
{devices.map(device => (
<React.Fragment key={device.id()}>
<Typography sx={{paddingY: 1, paddingX: 2}}>{device.name()}</Typography>
<Divider />
{deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => (
<ListItem key={i}>{controlAccordion(control)}</ListItem>
))
:
<Typography sx={{paddingY: 1, paddingX: 2}}>No Control Interactions For Device</Typography>
}
</React.Fragment>
))} ))}
</List> </List>
</React.Fragment> </React.Fragment>

View file

@ -1,6 +1,8 @@
import { Button, DialogActions, DialogContent, DialogTitle, MenuItem, Select } from "@mui/material"; import { Button, Checkbox, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { getMeasurements } from "pbHelpers/ComponentType"; import { Option } from "common/SearchSelect";
import { Component } from "models";
import { extension, getMeasurements } from "pbHelpers/ComponentType";
import { Measurement, Operator } from "pbHelpers/Enums"; import { Measurement, Operator } from "pbHelpers/Enums";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack"; import { quack } from "protobuf-ts/quack";
@ -9,19 +11,58 @@ import { useEffect, useState } from "react";
interface Props { interface Props {
open: boolean open: boolean
onClose: (refreshCallback: boolean) => void onClose: (refreshCallback: boolean) => void
linkedComponents: Map<string, Component>
componentDevices: Map<string, number>
//if we pass in a device id we could filter the components to only be for that device, this could be used for the control part //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?: number
} }
export default function NewObjectInteraction(props: Props){ export default function NewObjectInteraction(props: Props){
const { open, onClose } = props const { open, onClose, device, linkedComponents, componentDevices } = props
const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>( const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>(
quack.ComponentType.COMPONENT_TYPE_INVALID quack.ComponentType.COMPONENT_TYPE_INVALID
); );
const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]); const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]);
const [selectedAlertComponents, setSelectedAlertComponents] = useState<string[]>([]); const [selectedAlertComponents, setSelectedAlertComponents] = useState<string[]>([]);
const [validOptions, setValidOptions] = useState<Option[]>([])
const [validComponents, setValidComponents] = useState<Component[]>([])
useEffect(() => {},[]) useEffect(() => {
//if the device is passed in need to filter out possible components
let validCompList: Component[] = []
let validOptions: Option[] = []
linkedComponents.forEach((comp, key) => {
if(componentDevices.get(key) === device || !device){
validCompList.push(comp)
}
})
let included: quack.ComponentType[] = []
validCompList.forEach(comp => {
if(included.includes(comp.type())) return
let ext = extension(comp.type(), comp.subType())
if(device){
if(!ext.isController){
included.push(comp.type())
validOptions.push({
label: ext.friendlyName,
value: comp.type()
})
}
}else{
included.push(comp.type())
validOptions.push({
label: ext.friendlyName,
value: comp.type()
})
}
})
setValidOptions(validOptions)
setValidComponents(validCompList)
},[device, linkedComponents, componentDevices])
const close = (refresh: boolean) => { const close = (refresh: boolean) => {
@ -41,6 +82,39 @@ export default function NewObjectInteraction(props: Props){
return [condition]; return [condition];
}; };
//display the components that match the type that was selected for the new alert
const listMatchingComponents = () => {
let matching: Component[] = [];
validComponents.forEach(comp => {
if (comp.type() === newAlertComponentType) {
matching.push(comp);
}
});
return (
<List>
{matching.map(comp => (
<ListItem key={comp.key()}>
<ListItemIcon>
<Checkbox
checked={selectedAlertComponents.includes(comp.key())}
onChange={(_, checked) => {
let selected = selectedAlertComponents;
if (checked) {
selected.push(comp.key());
} else {
selected.splice(selected.indexOf(comp.key()), 1);
}
setSelectedAlertComponents([...selected]);
}}
/>
</ListItemIcon>
<ListItemText>{comp.name()}</ListItemText>
</ListItem>
))}
</List>
);
};
return ( return (
<ResponsiveDialog <ResponsiveDialog
open={open} open={open}
@ -64,7 +138,7 @@ export default function NewObjectInteraction(props: Props){
<MenuItem key={0} value={0}> <MenuItem key={0} value={0}>
Select Component Type Select Component Type
</MenuItem> </MenuItem>
{typeOptions.map(option => ( {validOptions.map(option => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
{option.label} {option.label}
</MenuItem> </MenuItem>
@ -73,21 +147,21 @@ export default function NewObjectInteraction(props: Props){
{/* list of checkboxes for the components that match that type */} {/* list of checkboxes for the components that match that type */}
{listMatchingComponents()} {listMatchingComponents()}
{/* have the conditions set here */} {/* have the conditions set here */}
{conditionInput()} {/* {conditionInput()} */}
{/* have the schedule set here */} {/* have the schedule set here */}
{scheduler()} {/* {scheduler()} */}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => close(false)} variant="contained"> <Button onClick={() => close(false)} variant="contained">
Cancel Cancel
</Button> </Button>
<Button {/* <Button
disabled={selectedAlertComponents.length === 0} disabled={selectedAlertComponents.length === 0}
onClick={addInteractionToComponents} onClick={addInteractionToComponents}
variant="contained" variant="contained"
color="primary"> color="primary">
Confirm Confirm
</Button> </Button> */}
</DialogActions> </DialogActions>
</ResponsiveDialog> </ResponsiveDialog>
) )

View file

@ -0,0 +1,107 @@
import { Button, List, ListItem, ListSubheader, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useNotificationAPI } from "providers";
import React, { useEffect, useRef, useState } from "react";
import { getThemeType } from "theme/themeType";
interface Props {
objectKey: string
objectType: pond.ObjectType
}
const useStyles = makeStyles(() => {
return ({
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 Notifications(props: Props){
const {objectKey, objectType} = props
const classes = useStyles();
const [recentNotifications, setRecentNotifications] = useState<pond.Notification[]>([]);
const [notificationsLoading, setNotificationsLoading] = useState(false)
const loadingRef = useRef(false)
const [{as}] = useGlobalState();
const notificationAPI = useNotificationAPI();
const [totalNotifications, setTotalNotifications] = useState(0)
//get the notifications for the components on an object
useEffect(() => {
if (loadingRef.current) return
loadingRef.current = true
notificationAPI
.listObjectNotifications(objectKey, objectType, 10, 0, undefined, undefined, undefined, as)
.then(resp => {
setRecentNotifications(resp.data.notifications);
setTotalNotifications(resp.data.total);
})
.catch(_err => {})
.finally(() => {
loadingRef.current = true;
});
}, [objectKey, objectType, notificationAPI]);
const loadMoreNotifications = () => {
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);
});
};
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>
);
}

View file

@ -9,23 +9,32 @@ import { useInteractionsAPI } from "hooks";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import { extension, getFriendlyName } from "pbHelpers/ComponentType"; import { extension, getFriendlyName } from "pbHelpers/ComponentType";
import { sameComponentID } from "pbHelpers/Component"; import { sameComponentID } from "pbHelpers/Component";
import Notifications from "./Notifications";
import { pond } from "protobuf-ts/pond";
import React from "react";
import NewObjectInteraction from "./NewObjectInteraction";
interface Props { interface Props {
linkedComponents: Map<string, Component>; //the component key to the component object linkedComponents: Map<string, Component>; //the component key to the component object
componentDevices: Map<string, number>; componentDevices: Map<string, number>;
objectKey: string
objectType: pond.ObjectType
devices: Device[]; devices: Device[];
refreshCallback?: () => {}
} }
export default function ObjectInteractions(props: Props) { export default function ObjectInteractions(props: Props) {
const { linkedComponents, componentDevices, devices } = props const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback } = props
const [{as}] = useGlobalState() const [{as}] = useGlobalState()
//list of alerts, each alert contains a list of interactions with matching conditions //list of alerts, each alert contains a list of interactions with matching conditions
const [alerts, setAlerts] = useState<Alert[]>([]); const [alerts, setAlerts] = useState<Alert[]>([]);
//list of interactions relating to a controller //list of interactions relating to a controller
const [controls, setControls] = useState<Control[]>([]); const [controls, setControls] = useState<Control[]>([]);
const interactionsAPI = useInteractionsAPI(); const interactionsAPI = useInteractionsAPI();
const [sinkOptions, setSinkOptions] = useState<Component[]>([]) const [openNewInteraction, setOpenNewInteraction] = useState(false)
const [typeOptions, setTypeOptions] = useState<Option[]>([]); const [allSinks, setAllSinks] = useState<Component[]>([]);
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
const [selectedDevice, setSelectedDevice] = useState<number | undefined>(undefined)
const matchConditions = (interaction: Interaction, set: Alert | Control) => { const matchConditions = (interaction: Interaction, set: Alert | Control) => {
@ -79,8 +88,6 @@ export default function ObjectInteractions(props: Props) {
useEffect(() => { useEffect(() => {
const newInteractions: Interaction[] = []; const newInteractions: Interaction[] = [];
const interactionSourceMap = new Map<string, string>(); const interactionSourceMap = new Map<string, string>();
const includedOps: quack.ComponentType[] = [];
const typeOps: Option[] = [];
const sinkOptions: Component[] = []; const sinkOptions: Component[] = [];
const alerts: Alert[] = []; const alerts: Alert[] = [];
const controls: Control[] = []; const controls: Control[] = [];
@ -103,15 +110,6 @@ export default function ObjectInteractions(props: Props) {
newInteractions.push(...resp); 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 // Collect controller components
if (extension(comp.type()).isController) { if (extension(comp.type()).isController) {
sinkOptions.push(comp); sinkOptions.push(comp);
@ -178,8 +176,7 @@ export default function ObjectInteractions(props: Props) {
} }
} }
}); });
setTypeOptions(typeOps); setAllSinks(sinkOptions);
setSinkOptions(sinkOptions);
setAlerts(alerts); setAlerts(alerts);
console.log(alerts) console.log(alerts)
setControls(controls); setControls(controls);
@ -192,13 +189,35 @@ export default function ObjectInteractions(props: Props) {
return ( return (
<Card raised style={{ margin: 5 }}> <React.Fragment>
<Box padding={1}> <NewObjectInteraction
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography> open={openNewInteraction}
<Controls controls={controls}/> onClose={(refresh) => {
<Alerts alerts={alerts}/> setOpenNewInteraction(false)
{/* <Notifications /> */} if(refresh && refreshCallback){
</Box> refreshCallback()
</Card> }
}}
linkedComponents={linkedComponents}
componentDevices={componentDevices}
device={selectedDevice}/>
<Card raised style={{ margin: 5 }}>
<Box padding={1}>
<Typography style={{ fontWeight: 650, fontSize: 25 }}>Interactions</Typography>
<Controls
controls={controls}
devices={devices}
addNew={(deviceID) => {
setSelectedDevice(deviceID)
setOpenNewInteraction(true)
}}/>
<Alerts alerts={alerts} addNew={() => {
setSelectedDevice(undefined)
setOpenNewInteraction(true)
}}/>
<Notifications objectKey={objectKey} objectType={objectType}/>
</Box>
</Card>
</React.Fragment>
) )
} }

View file

@ -857,56 +857,15 @@ export default function Bin(props: Props) {
} }
]} ]}
/> />
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={detail}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"inventory"}
aria-label="Inventory Details"
onClick={() => setDetail("inventory")}>
Inventory
</StyledToggle>
<StyledToggle
onClick={() => setDetail("sensors")}
value={"sensors"}
aria-label="Bin Sensor Graphs">
Sensors
</StyledToggle>
<StyledToggle
onClick={() => setDetail("analytics")}
value={"analytics"}
aria-label="Bin Analysis Graphs">
Analysis
</StyledToggle>
<StyledToggle
onClick={() => setDetail("alerts")}
value={"alerts"}
aria-label="Device Alerts">
Alerts
</StyledToggle>
<StyledToggle
onClick={() => setDetail("presets")}
value={"presets"}
aria-label="Bin Mode Presets">
Presets
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box> </Box>
{detail === "alerts" && ( {detail === "alerts" && (
<Box> <Box>
{/* <ObjectAlerts
linkedComponents={components}
componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
objectKey={bin.key()}
/> */}
<ObjectInteractions <ObjectInteractions
linkedComponents={components} linkedComponents={components}
componentDevices={componentDevices} componentDevices={componentDevices}
devices={devices} devices={devices}
objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
/> />
</Box> </Box>
)} )}
@ -1039,16 +998,12 @@ export default function Bin(props: Props) {
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={mobileTab} index={4}> <TabPanelMine value={mobileTab} index={4}>
<Box> <Box>
{/* <ObjectAlerts
linkedComponents={components}
componentDevices={componentDevices}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
objectKey={bin.key()}
/> */}
<ObjectInteractions <ObjectInteractions
linkedComponents={components} linkedComponents={components}
componentDevices={componentDevices} componentDevices={componentDevices}
devices={devices} devices={devices}
objectKey={bin.key()}
objectType={pond.ObjectType.OBJECT_TYPE_BIN}
/> />
</Box> </Box>
</TabPanelMine> </TabPanelMine>