diff --git a/src/objects/objectInteractions/Alerts.tsx b/src/objects/objectInteractions/Alerts.tsx index 0c1ab01..562216b 100644 --- a/src/objects/objectInteractions/Alerts.tsx +++ b/src/objects/objectInteractions/Alerts.tsx @@ -1,10 +1,11 @@ -import { ExpandMore } from "@mui/icons-material"; -import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, List, ListItem, ListSubheader, Typography } from "@mui/material"; +import { Add, ExpandMore } from "@mui/icons-material"; +import { Accordion, AccordionDetails, AccordionSummary, Box, Grid2, IconButton, 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"; +import { Option } from "common/SearchSelect"; export interface Alert { conditions: pond.InteractionCondition[]; @@ -13,11 +14,12 @@ export interface Alert { interface Props { alerts: Alert[] + addNew?: () => void } export default function Alerts(props: Props){ - const {alerts} = props - const [openNewAlert, setOpenNewAlert] = useState(false) + const {alerts, addNew} = props + // const [openNewAlert, setOpenNewAlert] = useState(false) const readableCondition = (condition: pond.InteractionCondition) => { let describer = describeMeasurement(condition.measurementType); @@ -62,9 +64,19 @@ export default function Alerts(props: Props){ return ( - {}}/> + {/* { + setOpenNewAlert(false) + if(refresh && refreshCallback) refreshCallback() + }} typeOptions={typeOptions} linkedComponents={linkedComponents}/> */} - Alerts + + + Alerts + + {addNew && + {addNew()}} color="primary"> + } + {alerts.map((alert, i) => ( {alertAccordion(alert)} ))} diff --git a/src/objects/objectInteractions/Controls.tsx b/src/objects/objectInteractions/Controls.tsx index 096c9ca..9498fed 100644 --- a/src/objects/objectInteractions/Controls.tsx +++ b/src/objects/objectInteractions/Controls.tsx @@ -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 { pond } from "protobuf-ts/pond"; import React, { useEffect, useState } from "react"; @@ -12,17 +13,20 @@ export interface Control { } interface Props { + devices: Device[] controls: Control[] + addNew?: (deviceID: number) => void } export default function Controls(props: Props){ - const { controls } = props + const { devices, controls, addNew } = props const [deviceControls, setDeviceControls] = useState>(new Map()) - const [deviceNameMap, setDeviceNameMap] = useState>(new Map()) + const [openDeviceMenu, setOpenDeviceMenu] = useState(false) + const [anchor, setAnchor] = useState(null) + useEffect(()=>{ let map: Map = new Map() - let nameMap: Map = new Map() controls.forEach(control => { let mappedControls = map.get(control.device.id()) if(mappedControls){ @@ -30,30 +34,72 @@ export default function Controls(props: Props){ }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 ( - {control.device.name()} - + Show what the control does + show the components that are doing it ) } + const deviceMenu = () => { + return ( + { + setAnchor(null) + setOpenDeviceMenu(false) + }}> + {devices.map(dev => ( + { + setAnchor(null) + setOpenDeviceMenu(false) + if (addNew) addNew(dev.id()) + + }}> + {dev.name()} + + ))} + + ) + } return ( + {deviceMenu()} - Controls - {controls.map((control, i) => ( - {controlAccordion(control)} + + + Alerts + + { + setOpenDeviceMenu(true) + setAnchor(e.currentTarget) + }} + color="primary"> + + + + {devices.map(device => ( + + {device.name()} + + {deviceControls.get(device.id()) ? deviceControls.get(device.id())?.map((control, i) => ( + {controlAccordion(control)} + )) + : + No Control Interactions For Device + } + ))} diff --git a/src/objects/objectInteractions/NewObjectInteraction.tsx b/src/objects/objectInteractions/NewObjectInteraction.tsx index 31170e3..b868fb7 100644 --- a/src/objects/objectInteractions/NewObjectInteraction.tsx +++ b/src/objects/objectInteractions/NewObjectInteraction.tsx @@ -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 { 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 { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; @@ -9,19 +11,58 @@ import { useEffect, useState } from "react"; interface Props { open: boolean onClose: (refreshCallback: boolean) => void + linkedComponents: Map + componentDevices: Map //if we pass in a device id we could filter the components to only be for that device, this could be used for the control part device?: number } export default function NewObjectInteraction(props: Props){ - const { open, onClose } = props + const { open, onClose, device, linkedComponents, componentDevices } = props const [newAlertComponentType, setNewAlertComponentType] = useState( quack.ComponentType.COMPONENT_TYPE_INVALID ); const [conditions, setConditions] = useState([]); const [selectedAlertComponents, setSelectedAlertComponents] = useState([]); + const [validOptions, setValidOptions] = useState([]) + const [validComponents, setValidComponents] = useState([]) - 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) => { @@ -41,6 +82,39 @@ export default function NewObjectInteraction(props: Props){ return [condition]; }; + //display the components that match the type that was selected for the new alert + const listMatchingComponents = () => { + let matching: Component[] = []; + validComponents.forEach(comp => { + if (comp.type() === newAlertComponentType) { + matching.push(comp); + } + }); + return ( + + {matching.map(comp => ( + + + { + let selected = selectedAlertComponents; + if (checked) { + selected.push(comp.key()); + } else { + selected.splice(selected.indexOf(comp.key()), 1); + } + setSelectedAlertComponents([...selected]); + }} + /> + + {comp.name()} + + ))} + + ); + }; + return ( Select Component Type - {typeOptions.map(option => ( + {validOptions.map(option => ( {option.label} @@ -73,21 +147,21 @@ export default function NewObjectInteraction(props: Props){ {/* list of checkboxes for the components that match that type */} {listMatchingComponents()} {/* have the conditions set here */} - {conditionInput()} + {/* {conditionInput()} */} {/* have the schedule set here */} - {scheduler()} + {/* {scheduler()} */} - + */} ) diff --git a/src/objects/objectInteractions/Notifications.tsx b/src/objects/objectInteractions/Notifications.tsx index e69de29..aa2e410 100644 --- a/src/objects/objectInteractions/Notifications.tsx +++ b/src/objects/objectInteractions/Notifications.tsx @@ -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([]); + 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 ( + + Notifications + {recentNotifications && ( + + {recentNotifications.map((notification, i) => ( + + + {moment(notification.status?.timestamp).format("MMMM DD, YYYY - h:mm a")} + {" "} + :{" "} + + {notification.settings?.title + " - " + notification.settings?.subtitle} + + + ))} + {recentNotifications.length < totalNotifications && ( + + + + )} + + )} + + ); +} \ No newline at end of file diff --git a/src/objects/objectInteractions/ObjectInteractions.tsx b/src/objects/objectInteractions/ObjectInteractions.tsx index aa9f269..51e3230 100644 --- a/src/objects/objectInteractions/ObjectInteractions.tsx +++ b/src/objects/objectInteractions/ObjectInteractions.tsx @@ -9,23 +9,32 @@ import { useInteractionsAPI } from "hooks"; import { useGlobalState } from "providers"; import { extension, getFriendlyName } from "pbHelpers/ComponentType"; 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 { linkedComponents: Map; //the component key to the component object componentDevices: Map; + objectKey: string + objectType: pond.ObjectType devices: Device[]; + refreshCallback?: () => {} } export default function ObjectInteractions(props: Props) { - const { linkedComponents, componentDevices, devices } = props + const { linkedComponents, componentDevices, devices, objectKey, objectType, refreshCallback } = props const [{as}] = useGlobalState() //list of alerts, each alert contains a list of interactions with matching conditions const [alerts, setAlerts] = useState([]); //list of interactions relating to a controller const [controls, setControls] = useState([]); const interactionsAPI = useInteractionsAPI(); - const [sinkOptions, setSinkOptions] = useState([]) - const [typeOptions, setTypeOptions] = useState([]); + const [openNewInteraction, setOpenNewInteraction] = useState(false) + const [allSinks, setAllSinks] = useState([]); + // const [typeOptions, setTypeOptions] = useState([]); + const [selectedDevice, setSelectedDevice] = useState(undefined) const matchConditions = (interaction: Interaction, set: Alert | Control) => { @@ -79,8 +88,6 @@ export default function ObjectInteractions(props: Props) { useEffect(() => { const newInteractions: Interaction[] = []; const interactionSourceMap = new Map(); - const includedOps: quack.ComponentType[] = []; - const typeOps: Option[] = []; const sinkOptions: Component[] = []; const alerts: Alert[] = []; const controls: Control[] = []; @@ -103,15 +110,6 @@ export default function ObjectInteractions(props: Props) { 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); @@ -178,8 +176,7 @@ export default function ObjectInteractions(props: Props) { } } }); - setTypeOptions(typeOps); - setSinkOptions(sinkOptions); + setAllSinks(sinkOptions); setAlerts(alerts); console.log(alerts) setControls(controls); @@ -192,13 +189,35 @@ export default function ObjectInteractions(props: Props) { return ( - - - Interactions - - - {/* */} - - + + { + setOpenNewInteraction(false) + if(refresh && refreshCallback){ + refreshCallback() + } + }} + linkedComponents={linkedComponents} + componentDevices={componentDevices} + device={selectedDevice}/> + + + Interactions + { + setSelectedDevice(deviceID) + setOpenNewInteraction(true) + }}/> + { + setSelectedDevice(undefined) + setOpenNewInteraction(true) + }}/> + + + + ) } \ No newline at end of file diff --git a/src/pages/Bin.tsx b/src/pages/Bin.tsx index 4776ec7..37c2d62 100644 --- a/src/pages/Bin.tsx +++ b/src/pages/Bin.tsx @@ -857,56 +857,15 @@ export default function Bin(props: Props) { } ]} /> - {/* - setDetail("inventory")}> - Inventory - - setDetail("sensors")} - value={"sensors"} - aria-label="Bin Sensor Graphs"> - Sensors - - setDetail("analytics")} - value={"analytics"} - aria-label="Bin Analysis Graphs"> - Analysis - - setDetail("alerts")} - value={"alerts"} - aria-label="Device Alerts"> - Alerts - - setDetail("presets")} - value={"presets"} - aria-label="Bin Mode Presets"> - Presets - - */} {detail === "alerts" && ( - {/* */} )} @@ -1039,16 +998,12 @@ export default function Bin(props: Props) { - {/* */}