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:
parent
3cdddb928f
commit
ce7c90f384
6 changed files with 314 additions and 101 deletions
|
|
@ -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 (
|
||||
<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 }}>
|
||||
<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) => (
|
||||
<ListItem key={i}>{alertAccordion(alert)}</ListItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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<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(()=>{
|
||||
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){
|
||||
|
|
@ -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 (
|
||||
<Accordion>
|
||||
<AccordionSummary>{control.device.name()}</AccordionSummary>
|
||||
<AccordionDetails></AccordionDetails>
|
||||
<AccordionSummary>Show what the control does</AccordionSummary>
|
||||
<AccordionDetails>show the components that are doing it</AccordionDetails>
|
||||
</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 (
|
||||
<React.Fragment>
|
||||
{deviceMenu()}
|
||||
<List style={{ margin: 5 }}>
|
||||
<ListSubheader>Controls</ListSubheader>
|
||||
{controls.map((control, i) => (
|
||||
<ListItem key={i}>{controlAccordion(control)}</ListItem>
|
||||
<ListSubheader sx={{paddingY: 1, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
|
||||
<Typography>
|
||||
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>
|
||||
</React.Fragment>
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
}
|
||||
|
||||
export default function NewObjectInteraction(props: Props){
|
||||
const { open, onClose } = props
|
||||
const { open, onClose, device, linkedComponents, componentDevices } = props
|
||||
const [newAlertComponentType, setNewAlertComponentType] = useState<quack.ComponentType>(
|
||||
quack.ComponentType.COMPONENT_TYPE_INVALID
|
||||
);
|
||||
const [conditions, setConditions] = useState<pond.InteractionCondition[]>([]);
|
||||
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) => {
|
||||
|
|
@ -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 (
|
||||
<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 (
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
|
|
@ -64,7 +138,7 @@ export default function NewObjectInteraction(props: Props){
|
|||
<MenuItem key={0} value={0}>
|
||||
Select Component Type
|
||||
</MenuItem>
|
||||
{typeOptions.map(option => (
|
||||
{validOptions.map(option => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
|
|
@ -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()} */}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => close(false)} variant="contained">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
disabled={selectedAlertComponents.length === 0}
|
||||
onClick={addInteractionToComponents}
|
||||
variant="contained"
|
||||
color="primary">
|
||||
Confirm
|
||||
</Button>
|
||||
</Button> */}
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<string, Component>; //the component key to the component object
|
||||
componentDevices: Map<string, number>;
|
||||
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<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 [openNewInteraction, setOpenNewInteraction] = useState(false)
|
||||
const [allSinks, setAllSinks] = useState<Component[]>([]);
|
||||
// const [typeOptions, setTypeOptions] = useState<Option[]>([]);
|
||||
const [selectedDevice, setSelectedDevice] = useState<number | undefined>(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<string, string>();
|
||||
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 (
|
||||
<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>
|
||||
<React.Fragment>
|
||||
<NewObjectInteraction
|
||||
open={openNewInteraction}
|
||||
onClose={(refresh) => {
|
||||
setOpenNewInteraction(false)
|
||||
if(refresh && refreshCallback){
|
||||
refreshCallback()
|
||||
}
|
||||
}}
|
||||
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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue