imported the gate and terminal related files to prep for the aviation map

This commit is contained in:
csawatzky 2025-03-28 15:54:11 -06:00
parent e2e061151b
commit 505cb8e3aa
25 changed files with 4868 additions and 9 deletions

View file

@ -0,0 +1,131 @@
import { useTheme } from "@mui/material";
import moment from "moment";
import React, { useEffect, useState } from "react";
import {
Area,
AreaChart,
ReferenceArea,
ReferenceLine,
ResponsiveContainer,
Tooltip,
TooltipProps,
XAxis,
YAxis
} from "recharts";
import MaterialChartTooltip from "./MaterialChartTooltip";
export interface SSAreaDataPoint {
timestamp: number;
value: number;
}
interface Props {
data: SSAreaDataPoint[];
maxRef?: number;
minRef?: number;
newXDomain?: number[] | string[];
multiGraphZoom?: (domain: number[] | string[]) => void;
}
export default function SingleSetAreaChart(props: Props) {
const { data, maxRef, minRef, newXDomain, multiGraphZoom } = props;
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
const [refLeft, setRefLeft] = useState<number | undefined>();
const [refRight, setRefRight] = useState<number | undefined>();
const theme = useTheme();
const now = moment();
useEffect(() => {
if (newXDomain) {
setXDomain(newXDomain);
}
}, [newXDomain]);
const zoom = () => {
let newDomain: number[] | string[] = ["dataMin", "dataMax"];
if (refLeft && refRight && refLeft !== refRight) {
refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]);
setRefLeft(undefined);
setRefRight(undefined);
if (multiGraphZoom) {
multiGraphZoom(newDomain);
} else {
setXDomain(newDomain);
}
}
};
return (
<React.Fragment>
<ResponsiveContainer width={"100%"} height={270}>
<AreaChart
data={data}
onMouseDown={(e: any) => {
if (e) {
setRefLeft(e.activeLabel);
}
}}
onMouseMove={(e: any) => {
if (e) {
setRefRight(e.activeLabel);
}
}}
onMouseUp={() => {
setRefLeft(undefined);
setRefRight(undefined);
zoom();
}}>
<Tooltip
animationEasing="ease-out"
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
labelFormatter={timestamp => moment(timestamp).format("lll")}
content={(props: TooltipProps<any, any>) => (
<MaterialChartTooltip {...props} valueFormatter={value => `${value}`} />
)}
/>
<XAxis
allowDataOverflow
dataKey="timestamp"
domain={xDomain}
name="Time"
tickFormatter={timestamp => {
let t = moment(timestamp);
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
}}
scale="time"
type="number"
tick={{ fill: theme.palette.text.primary }}
stroke={theme.palette.divider}
interval="preserveStartEnd"
/>
<YAxis
type="number"
domain={["auto", "auto"]}
label={{
value: "Mass Flow (kg/s)",
position: "insideLeft",
angle: -90,
fill: theme.palette.text.primary
}}
/>
<Area dataKey={"value"} strokeWidth={3} />
<ReferenceLine
ifOverflow="extendDomain"
y={maxRef}
stroke="red"
strokeDasharray={"5 2"}
/>
<ReferenceLine
ifOverflow="extendDomain"
y={minRef}
stroke="red"
strokeDasharray={"5 2"}
/>
{refLeft && refRight ? (
<ReferenceArea x1={refLeft} x2={refRight} strokeOpacity={0.3} />
) : null}
</AreaChart>
</ResponsiveContainer>
</React.Fragment>
);
}

View file

@ -0,0 +1,323 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Checkbox,
CircularProgress,
Drawer,
List,
ListItem,
ListItemText,
Tab,
Tabs,
TextField
} from "@mui/material";
import { ExpandMore } from "@mui/icons-material";
import SearchBar from "common/SearchBar";
import { useDeviceAPI, useMobile } from "hooks";
import { Component, Device } from "models";
import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import { makeStyles } from "@mui/styles";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
const useStyles = makeStyles(() => ({
drawerPaperDesktop: {
height: "100%",
width: "40%"
},
drawerPaperMobile: {
height: "60%",
width: "100%"
}
}));
interface Props {
open: boolean;
deviceTags?: string[];
close: (reload?: boolean) => void;
linkedDevices: Map<string, pond.ComprehensiveDevice>;
updateLinkedDevices: (dev: pond.ComprehensiveDevice, linked: boolean) => void;
linkedComponents?: Component[];
updateLinkedComponents?: (
deviceID: string | number,
componen: Component,
linked: boolean
) => void;
//all of these need to be sent in for the device preference selector to work
devicePrefMap?: Map<number, number>; //map using the device id as the key and its pref enums value as the value
prefOptions?: JSX.Element[];
devicePrefChanged?: (device: Device, newPref: number) => void;
}
export default function DeviceLinkDrawer(props: Props) {
const {
open,
close,
linkedDevices,
linkedComponents,
updateLinkedDevices,
updateLinkedComponents,
deviceTags,
devicePrefMap,
prefOptions,
devicePrefChanged
} = props;
const [value, setValue] = useState(0);
const deviceAPI = useDeviceAPI();
const classes = useStyles();
const isMobile = useMobile();
const [deviceList, setDeviceList] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [searchVal, setSearchVal] = useState("");
const [searchedDevices, setSearchedDevices] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [accordionController, setAccordionController] = useState<boolean[]>([]);
const [loadingDevices, setLoadingDevices] = useState(false);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
useEffect(() => {
if (loadingDevices) return;
setLoadingDevices(true);
deviceAPI
.list(
500,
0,
"asc",
"key",
deviceTags?.toString(),
undefined,
undefined,
undefined,
undefined,
true
)
.then(resp => {
let devMap = new Map<string, pond.ComprehensiveDevice>();
resp.data.comprehensiveDevices.forEach(compDev => {
if (compDev.device?.settings?.deviceId) {
devMap.set(compDev.device?.settings?.deviceId.toString(), compDev);
}
});
setDeviceList(devMap);
setSearchedDevices(devMap);
})
.catch(err => {})
.finally(() => {
setLoadingDevices(false);
});
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [deviceAPI]);
useEffect(() => {
let searchedDevices: Map<string, pond.ComprehensiveDevice> = new Map();
if (searchVal !== "") {
deviceList.forEach(dev => {
if (dev.device?.settings?.name) {
if (
dev.device?.settings?.name.toLowerCase().includes(searchVal.toLowerCase()) ||
dev.device?.settings?.deviceId.toString().includes(searchVal)
) {
searchedDevices.set(dev.device.settings.deviceId.toString(), dev);
}
} else {
if (dev.device?.settings?.deviceId.toString().includes(searchVal)) {
searchedDevices.set(dev.device.settings.deviceId.toString(), dev);
}
}
});
setSearchedDevices(searchedDevices);
} else {
setSearchedDevices(deviceList);
}
}, [searchVal, deviceList]);
//sets the length of the array of booleans to control opening each of the accordions
useEffect(() => {
let accordionControls: boolean[] = [];
linkedDevices.forEach(() => {
accordionControls.push(false);
});
deviceList.forEach(dev => {
if (
dev.device?.settings?.deviceId &&
!linkedDevices.get(dev.device?.settings?.deviceId.toString())
) {
accordionControls.push(false);
}
});
setAccordionController(accordionControls);
}, [linkedDevices, deviceList]);
const closeDrawer = () => {
close();
};
const devicesTab = () => {
return (
<Box>
Select Devices to link
<List>
<SearchBar
value={searchVal}
onChange={val => {
setSearchVal(val);
}}
/>
{Array.from(searchedDevices.values()).map(dev => {
let defCheck = linkedDevices.has(dev.device?.settings?.deviceId.toString() ?? "");
return (
<ListItem key={dev.device?.settings?.deviceId}>
<ListItemText
primary={dev.device?.settings?.name ?? "Device " + dev.device?.settings?.deviceId}
/>
<Checkbox
onChange={(_, checked) => {
if (dev.device?.settings?.deviceId) {
updateLinkedDevices(dev, checked);
}
}}
edge="end"
defaultChecked={defCheck}
inputProps={{ "aria-labelledby": dev.device?.settings?.name }}
/>
</ListItem>
);
})}
</List>
</Box>
);
};
const linkedDevicesTab = () => {
let linkOptions: pond.ComprehensiveDevice[] = [];
Array.from(linkedDevices.keys()).forEach(key => {
let dev = deviceList.get(key);
let linkedDev = linkedDevices.get(key);
if (dev) {
linkOptions.push(dev);
} else if (linkedDev) {
linkOptions.push(linkedDev);
}
});
return (
<Box>
From the linked Devices select which components to use
{linkOptions.map((op, i) => {
let device = Device.any(op.device);
let pref = 0;
if (devicePrefMap) {
pref = devicePrefMap.get(device.id()) ?? 0;
}
return (
<Accordion
key={device.id()}
expanded={accordionController[i]}
onChange={(_, expanded) => {
let accordionControls = accordionController;
accordionControls[i] = expanded;
setAccordionController([...accordionControls]);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>{device.name()}</AccordionSummary>
<AccordionDetails>
<List>
{op.components.map(comp => {
let component = Component.any(comp);
return (
<ListItem key={component.key()}>
<ListItemText primary={component.name()} />
<Checkbox
onChange={(_, checked) => {
if (component.key() && device.id() && updateLinkedComponents) {
updateLinkedComponents(device.id(), component, checked);
}
}}
edge="end"
checked={
linkedComponents &&
linkedComponents.some(el => el.key() === component.key())
}
inputProps={{ "aria-labelledby": component.name() }}
/>
</ListItem>
);
})}
</List>
{devicePrefMap && prefOptions && devicePrefChanged && (
<TextField
title="Gate Device Type"
select
value={pref}
variant="outlined"
onChange={e => {
pref = parseInt(e.target.value);
devicePrefChanged(device, pref);
}}
fullWidth>
{prefOptions}
</TextField>
)}
</AccordionDetails>
</Accordion>
);
})}
</Box>
);
};
return (
<Drawer
open={open}
onClose={closeDrawer}
classes={{ paper: isMobile ? classes.drawerPaperMobile : classes.drawerPaperDesktop }}
anchor={isMobile ? "bottom" : "right"}>
{open && (
<React.Fragment>
<Tabs value={value} indicatorColor="primary" textColor="primary" onChange={handleChange}>
<Tab label="Devices" />
{linkedComponents && <Tab label="Linked Devices" />}
</Tabs>
<TabPanelMine value={value} index={0}>
{loadingDevices ? (
<Box style={{ textAlign: "center", paddingTop: 20 }}>
<CircularProgress />
</Box>
) : (
devicesTab()
)}
</TabPanelMine>
{linkedComponents && (
<TabPanelMine value={value} index={1}>
{linkedDevicesTab()}
</TabPanelMine>
)}
</React.Fragment>
)}
</Drawer>
);
}

View file

@ -0,0 +1,105 @@
import { pond } from "protobuf-ts/pond";
import { Option } from "common/SearchSelect";
export interface InsulationGrade {
grade: string;
thermalResistance: number;
}
export interface DuctExtension {
name: string;
frictionFactor: number;
thermalConductivity: number;
thermalResistance: number;
}
const defaultDuct: DuctExtension = {
name: "",
frictionFactor: 0.0032,
thermalResistance: 9.343,
thermalConductivity: 0.036
};
export const DuctExtensions: Map<pond.DuctType, DuctExtension> = new Map([
[pond.DuctType.DUCT_TYPE_INVALID, defaultDuct],
[pond.DuctType.DUCT_TYPE_NONE, defaultDuct],
[
pond.DuctType.DUCT_TYPE_LAYFLAT,
{
name: "AvroDuct Layflat",
frictionFactor: 0.0032,
thermalResistance: 9.343,
thermalConductivity: 0.036
}
],
[
pond.DuctType.DUCT_TYPE_SPIRAL,
{
name: "AvroDuct Spiral",
frictionFactor: 0.0065,
thermalResistance: 9.343,
thermalConductivity: 0.036
}
],
[
pond.DuctType.DUCT_TYPE_AVROLITE,
{
name: "Avrolite",
frictionFactor: 0.0032,
thermalResistance: 10.132,
thermalConductivity: 0.036
}
],
[
pond.DuctType.DUCT_TYPE_AVROTUBE,
{
name: "AvroTube",
frictionFactor: 0.0032,
thermalResistance: 9.343,
thermalConductivity: 0.036
}
]
]);
export default function DuctDescriber(type: pond.DuctType): DuctExtension {
let describer = DuctExtensions.get(type);
//console.log(describer)
if (describer?.name === "None") {
//console.trace()
}
return describer ? describer : defaultDuct;
}
export const ToDuctOption = (ductType: pond.DuctType): Option => {
let duct = DuctDescriber(ductType);
return {
value: ductType,
label: duct.name
} as Option;
};
export function DuctOptions(): Option[] {
let options: Option[] = [];
Object.values(pond.DuctType).forEach(ductType => {
if (
typeof ductType !== "string" &&
ductType !== pond.DuctType.DUCT_TYPE_INVALID &&
ductType !== pond.DuctType.DUCT_TYPE_NONE
) {
options.push(ToDuctOption(ductType));
}
});
return options;
}
export function FindDuctType(tRes: number, tCond: number, friction: number): pond.DuctType {
let ductType = pond.DuctType.DUCT_TYPE_NONE;
DuctExtensions.forEach((duct, type) => {
if (
tRes === duct.thermalResistance &&
tCond === duct.thermalConductivity &&
friction === duct.frictionFactor
) {
ductType = type;
}
});
return ductType;
}

57
src/gate/AddGateFab.tsx Normal file
View file

@ -0,0 +1,57 @@
import { Fab, Theme } from "@mui/material";
import Icon from "assets/products/Aviation/AddPlaneIconBlack.png";
import { ImgIcon } from "common/ImgIcon";
import { useMobile } from "hooks";
import { makeStyles } from "@mui/styles";
interface Props {
onClick: () => void;
pulse: boolean;
}
const useStyles = makeStyles((theme: Theme) => ({
"@keyframes pulsate": {
to: {
boxShadow: "0 0 0 16px" + theme.palette.primary.main + "00"
}
},
fab: {
background: theme.palette.primary.main,
"&:hover": {
background: theme.palette.primary.main
},
"&:focus": {
background: theme.palette.primary.main
},
position: "fixed",
bottom: theme.spacing(8), //for mobile navigator
right: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
bottom: theme.spacing(2)
}
},
pulse: {
boxShadow: "0 0 0 0 " + theme.palette.primary.main + "75",
animation: "$pulsate 1.75s infinite cubic-bezier(0.66, 0.33, 0, 1)"
}
}));
export default function AddFab(props: Props) {
const { onClick, pulse } = props;
const classes = useStyles();
const isMobile = useMobile();
const pulseString = pulse ? classes.pulse : "";
const classString = classes.fab + " " + pulseString;
return (
<Fab
onClick={onClick}
aria-label="Add New Gate"
className={classString}
size={isMobile ? "medium" : "large"}>
<ImgIcon alt="Add New Gate" src={Icon} />
</Fab>
);
}

203
src/gate/GateActions.tsx Normal file
View file

@ -0,0 +1,203 @@
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Tooltip
} from "@mui/material";
import SettingsIcon from "@mui/icons-material/Settings";
import MoreIcon from "@mui/icons-material/MoreVert";
import React, { useState } from "react";
import GateSettings from "./GateSettings";
import { Gate } from "models/Gate";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import ObjectUsers from "user/ObjectUsers";
import { pond } from "protobuf-ts/pond";
import { Scope } from "models";
import ObjectTeams from "teams/ObjectTeams";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { blue } from "@mui/material/colors";
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
import { Share } from "@mui/icons-material";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles(() => ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
},
blueIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
}
}));
interface OpenState {
users: boolean;
teams: boolean;
settings: boolean;
removeSelf: boolean;
share: boolean;
}
interface Props {
gate: Gate;
refreshCallback: () => void;
permissions: pond.Permission[];
}
export default function GateActions(props: Props) {
const classes = useStyles();
const { gate, refreshCallback, permissions } = props;
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [openState, setOpenState] = useState<OpenState>({
users: false,
teams: false,
settings: false,
removeSelf: false,
share: false
});
const groupMenu = () => {
return (
<Menu
id="menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
<MenuItem
onClick={() => {
setOpenState({ ...openState, share: true });
setAnchorEl(null);
}}
dense>
<ListItemIcon>
<Share className={classes.blueIcon} />
</ListItemIcon>
<ListItemText secondary="Share" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, users: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, teams: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setOpenState({ ...openState, removeSelf: true });
setAnchorEl(null);
}}>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const dialogs = () => {
const key = gate.key;
const label = gate.name;
return (
<React.Fragment>
<GateSettings
gate={gate}
open={openState.settings}
close={newGate => {
if (newGate) {
}
setOpenState({ ...openState, settings: false });
}}
/>
<ShareObject
scope={{ kind: "gate", key: key } as Scope}
label={label}
permissions={permissions}
isDialogOpen={openState.share}
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
/>
<ObjectUsers
scope={{ kind: "gate", key: key } as Scope}
label={label}
permissions={permissions}
isDialogOpen={openState.users}
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
<ObjectTeams
scope={{ kind: "gate", key: key } as Scope}
label={label}
permissions={permissions}
isDialogOpen={openState.teams}
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
refreshCallback={refreshCallback}
/>
<RemoveSelfFromObject
scope={{ kind: "gate", key: key } as Scope}
path={"terminal"}
label={label}
isDialogOpen={openState.removeSelf}
closeDialogCallback={() => {
setOpenState({ ...openState, removeSelf: false });
}}
/>
</React.Fragment>
);
};
return (
<React.Fragment>
<Tooltip title="Settings">
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
<SettingsIcon />
</IconButton>
</Tooltip>
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
{dialogs()}
{groupMenu()}
</React.Fragment>
);
}

413
src/gate/GateDevice.tsx Normal file
View file

@ -0,0 +1,413 @@
import {
Box,
Button,
Card,
Grid2 as Grid,
MenuItem,
Select,
ToggleButton,
ToggleButtonGroup,
Typography
} from "@mui/material";
import { Component, Device } from "models";
import { Gate } from "models/Gate";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useGateAPI } from "providers";
import { useEffect, useState } from "react";
import { useMobile, useSnackbar } from "hooks";
import { UnitMeasurement } from "models/UnitMeasurement";
import { quack } from "protobuf-ts/quack";
import GateDeviceInteraction from "./GateDeviceInteraction";
import GateSVG from "./GateSVG";
import GateGraphs from "./GateGraphs";
//import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import { getThemeType } from "theme";
interface Props {
gate: Gate;
comprehensiveDevice: pond.ComprehensiveDevice;
linkedCompList: Component[];
drawerView?: boolean;
}
export default function GateDevice(props: Props) {
const { gate, comprehensiveDevice, linkedCompList, drawerView } = props;
const [device, setDevice] = useState<Device>(Device.create());
const [componentOptions, setComponentOptions] = useState<Map<string, Component>>(
new Map<string, Component>()
);
const gateAPI = useGateAPI();
const [tempKey, setTempKey] = useState("");
const [pressureKey, setPressureKey] = useState("");
const [ambientKey, setAmbientKey] = useState("");
const { openSnack } = useSnackbar();
const [{ user }] = useGlobalState();
const [interactionDialog, setInteractionDialog] = useState(false);
const [densityTemp, setDensityTemp] = useState(0);
const isMobile = useMobile();
const [pcaState, setPCAState] = useState(false);
const [pcaFanOn, setPCAFanOn] = useState(false);
const [detail, setDetail] = useState<"sensors" | "analytics">("analytics");
const [lastAmbient, setLastAmbient] = useState(0);
const [lastTemps, setLastTemps] = useState({ t1: 0, t2: 0 });
const [lastPressures, setLastPressures] = useState({ p1: 0, p2: 0 });
// const StyledToggleButtonGroup = withStyles(theme => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }))(ToggleButtonGroup);
// const StyledToggle = withStyles({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// })(ToggleButton);
useEffect(() => {
if (comprehensiveDevice.device) {
setDevice(Device.any(comprehensiveDevice.device));
}
if (comprehensiveDevice.components) {
let components: Map<string, Component> = new Map<string, Component>();
setAmbientKey("");
setTempKey("");
setPressureKey("");
comprehensiveDevice.components.forEach(comp => {
let c = Component.any(comp);
if (linkedCompList.some(el => el.key() === c.key())) {
components.set(c.key(), c);
//determine the positioned components using the gates preferences for it components
switch (gate.preferences[c.key()]) {
case pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT:
setAmbientKey(c.key());
c.status.measurement.forEach(um => {
let measurement = UnitMeasurement.any(um, user);
if (measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
if (measurement.values.length > 1 && measurement.values[0].values.length > 0) {
setDensityTemp(measurement.values[0].values[0]);
}
}
});
break;
case pond.GateComponentType.GATE_COMPONENT_TYPE_TEMP:
setTempKey(c.key());
break;
case pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE:
setPressureKey(c.key());
if (
c.status.measurement[0] &&
c.status.measurement[0].values[0] &&
c.status.measurement[0].values[0].values[1]
) {
setPCAFanOn(c.status.measurement[0].values[0].values[1] > 250); //apx 1 iwg
}
break;
default:
// type is unknown, do nothing
break;
}
}
});
setComponentOptions(components);
}
}, [comprehensiveDevice, gate, linkedCompList, user]);
const gateComponentUpdate = (componentKey: string, gateCompType: pond.GateComponentType) => {
if (componentKey !== "") {
gateAPI
.updatePrefs(
gate.key,
"component",
device.id() + ":" + componentKey,
gateCompType,
[gate.key],
["gate"]
)
.then(resp => {
openSnack("Component Prefence Updated");
});
}
};
useEffect(() => {
let tempComponent = componentOptions.get(tempKey);
let t1 = 0;
let t2 = 0;
if (tempComponent) {
tempComponent.status.measurement.forEach(um => {
let measurement = UnitMeasurement.any(um, user);
if (
measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE &&
measurement.values.length > 0
) {
let nodeVals = measurement.values[0].values;
if (nodeVals.length > 1) {
t1 = nodeVals[0]; //uses the first node
t2 = nodeVals[1]; //uses second node node
}
}
});
}
setLastTemps({ t1, t2 });
}, [componentOptions, tempKey, user]);
useEffect(() => {
let ambientComponent = componentOptions.get(ambientKey);
let temp = 0;
if (ambientComponent) {
ambientComponent.status.measurement.forEach(um => {
let measurement = UnitMeasurement.any(um, user);
if (measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
if (measurement.values.length > 0 && measurement.values[0].values.length > 0) {
temp = measurement.values[0].values[0];
}
}
});
}
setLastAmbient(temp);
}, [componentOptions, ambientKey, user]);
useEffect(() => {
let pressureComponent = componentOptions.get(pressureKey);
let p1 = 0;
let p2 = 0;
if (pressureComponent) {
pressureComponent.status.measurement.forEach(um => {
let measurement = UnitMeasurement.any(um, user);
if (
measurement.type === quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE &&
measurement.values.length > 0
) {
let nodeVals = measurement.values[0].values;
if (nodeVals.length > 1) {
p1 = nodeVals[0];
p2 = nodeVals[1];
}
}
});
}
setLastPressures({ p1, p2 });
}, [componentOptions, pressureKey, user]);
const ambientSelector = () => {
return (
<Select
id="ambientSelect"
value={ambientKey}
style={{ fontSize: 8 }}
onChange={e => {
var key = e.target.value as string;
gateComponentUpdate(key, pond.GateComponentType.GATE_COMPONENT_TYPE_AMBIENT);
setAmbientKey(key);
if (tempKey === key) {
setTempKey("");
}
if (pressureKey === key) {
setPressureKey("");
}
}}
displayEmpty>
<MenuItem value="">
<em>Select Ambient Sensor</em>
</MenuItem>
{Array.from(componentOptions.values()).map(c => {
return (
<MenuItem key={c.key()} value={c.key()}>
{c.name()}
</MenuItem>
);
})}
</Select>
);
};
const tempSelector = () => {
return (
<Select
id="tempSelect"
value={tempKey}
style={{ fontSize: 8 }}
onChange={e => {
gateComponentUpdate(
e.target.value as string,
pond.GateComponentType.GATE_COMPONENT_TYPE_TEMP
);
setTempKey(e.target.value as string);
if (pressureKey === e.target.value) {
setPressureKey("");
}
if (ambientKey === e.target.value) {
setAmbientKey("");
}
}}
displayEmpty>
<MenuItem value="">
<em>Select Temperature Chain</em>
</MenuItem>
{Array.from(componentOptions.values()).map(c => {
return (
<MenuItem key={c.key()} value={c.key()}>
{c.name()}
</MenuItem>
);
})}
</Select>
);
};
const pressureSelector = () => {
return (
<Select
id="pressureSelect"
value={pressureKey}
style={{ fontSize: 8 }}
onChange={e => {
gateComponentUpdate(
e.target.value as string,
pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
);
setPressureKey(e.target.value as string);
if (tempKey === e.target.value) {
setTempKey("");
}
if (ambientKey === e.target.value) {
setAmbientKey("");
}
}}
displayEmpty>
<MenuItem value="">
<em>Select Pressure Chain</em>
</MenuItem>
{Array.from(componentOptions.values()).map(c => {
return (
<MenuItem key={c.key()} value={c.key()}>
{c.name()}
</MenuItem>
);
})}
</Select>
);
};
return (
<Grid
container
direction="row"
alignContent="center"
//alignItems="center"
>
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 8}} style={{ padding: 10 }}>
<Box
paddingLeft={1}
display="flex"
justifyContent="space-between"
alignItems="center"
marginBottom={2}>
<Typography style={{ fontWeight: 650, fontSize: 20 }}>{device.name()}</Typography>
<ToggleButtonGroup value={detail} exclusive size="small" aria-label="detail">
<ToggleButton
onClick={() => setDetail("analytics")}
value={"analytics"}
aria-label="Analysis Graphs">
Analysis
</ToggleButton>
<ToggleButton
onClick={() => setDetail("sensors")}
value={"sensors"}
aria-label="Sensor Graphs">
Sensors
</ToggleButton>
</ToggleButtonGroup>
</Box>
<Card raised>
<GateSVG
finalTemp={
gate.gateMutations[device.id().toString()] &&
!isNaN(gate.gateMutations[device.id().toString()].temp)
? gate.gateMutations[device.id().toString()].temp
: 0
}
ambientTemp={lastAmbient}
innerTemps={lastTemps}
innerPressures={lastPressures}
ambientSelector={ambientSelector}
tempChainSelector={tempSelector}
pressureChainSelector={pressureSelector}
pcaState={pcaState}
pcaFanState={pcaFanOn}
checkInTime={device.status.lastActive}
/>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {
setInteractionDialog(true);
}}>
Set Interactions
</Button>
</Card>
<GateDeviceInteraction
open={interactionDialog}
gate={gate}
compDevice={comprehensiveDevice}
densityTemp={densityTemp}
close={canceled => {
setInteractionDialog(false);
}}
/>
</Grid>
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 8}} style={{ padding: 10 }}>
<GateGraphs
gate={gate}
display={detail}
compMap={componentOptions}
setPCAState={(state: boolean) => {
setPCAState(state);
}}
ambient={ambientKey}
pressure={pressureKey}
device={device.id()}
/>
</Grid>
</Grid>
);
}

View file

@ -0,0 +1,255 @@
import { Button, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Component, Device } from "models";
import { Gate } from "models/Gate";
import moment from "moment";
import { pond, quack } from "protobuf-ts/pond";
import { useGlobalState, useInteractionsAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
interface Props {
open: boolean;
close: (canceled: boolean) => void;
gate: Gate;
compDevice: pond.ComprehensiveDevice;
densityTemp: number;
}
//map that the temp is the key and the air density is the value
const densityMap = new Map<number, number>([
[35, 1.15],
[30, 1.16],
[25, 1.18],
[20, 1.2],
[15, 1.23],
[10, 1.25],
[5, 1.27],
[0, 1.29],
[-5, 1.32],
[-10, 1.34],
[-15, 1.37],
[-20, 1.39],
[-25, 1.42]
]);
//this entire component will only be used for manual setting of each device on the gate
//once we have numbers for presets it may never be used again
export default function GateDeviceInteraction(props: Props) {
const { open, close, gate, compDevice, densityTemp } = props;
const [{ user }] = useGlobalState();
const [lowDelta, setLowDelta] = useState(0);
const [highDelta, setHighDelta] = useState(0);
const [greenComponent, setGreenComponent] = useState<Component>();
const [redComponent, setRedComponent] = useState<Component>();
const [pressureComponent, setPressureComponent] = useState<Component>();
const interactionsAPI = useInteractionsAPI();
const [adding, setAdding] = useState(false);
const { openSnack } = useSnackbar();
useEffect(() => {
//math to determine what the delta pressures to set will be
let celciusTemp = densityTemp;
if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) {
celciusTemp = densityTemp * 1.8 + 32;
}
let roundedTemp = Math.round(celciusTemp / 5) * 5;
let airDensity = densityMap.get(roundedTemp);
//have default values if the temp is outside the map
if (roundedTemp > 35 && airDensity === undefined) airDensity = 1.1;
if (roundedTemp < -25 && airDensity === undefined) airDensity = 1.5;
//calculate the c value for the equation
if (airDensity !== undefined) {
let diameterM = gate.ductDiameter() / 1000;
let pieRadSquare = 3.14 * Math.pow(diameterM / 2, 2);
let c = 0.98 * pieRadSquare * Math.sqrt(2 * airDensity);
let qmHigh = gate.upperFlow();
let qmLow = gate.lowerFlow();
setLowDelta(Math.round(Math.pow(qmLow / c, 2)));
setHighDelta(Math.round(Math.pow(qmHigh / c, 2)));
}
//addresses of controller LEDs on a V1 device
let redAddr = "3-1-512";
let greenAddr = "3-1-1024";
if (compDevice.device) {
let dev = Device.create(compDevice.device);
if (dev.settings.product === pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2) {
redAddr = "3-1-256";
greenAddr = "3-1-512";
}
}
//need to find if they have LED's in both of the controller positions
compDevice.components.forEach(comp => {
let component = Component.any(comp);
//checks the address for the LED components to get the red and green LED's
if (component.locationString() === redAddr && component.subType() === 1) {
setRedComponent(component);
} else if (component.locationString() === greenAddr && component.subType() === 1) {
setGreenComponent(component);
} else if (component.type() === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE) {
if (
gate.preferences[component.key()] === pond.GateComponentType.GATE_COMPONENT_TYPE_PRESSURE
) {
setPressureComponent(component);
}
}
});
}, [gate, densityTemp, user, compDevice]);
// useEffect(() => {
// //load current interactions for the device
// let deviceID = Device.any(compDevice.device).id();
// interactionsAPI.listInteractionsByDevice(deviceID).then(resp => {
// setCurrentInteractions(resp);
// });
// }, [compDevice.device, interactionsAPI]);
// const removeCurrentInteractions = () => {
// let deviceID = Device.any(compDevice.device).id();
// currentInteractions.forEach(interaction => {
// interactionsAPI.removeInteraction(deviceID, interaction.key());
// });
// };
const createInteractions = async () => {
//the interactions to be made
//TOGGLE green ON when pressure within range of upper and lower
if (
greenComponent !== undefined &&
redComponent !== undefined &&
pressureComponent !== undefined
) {
let greenToggle: pond.InteractionSettings = pond.InteractionSettings.create({
sink: quack.ComponentID.create({
type: greenComponent.type(),
address: greenComponent.settings.address,
addressType: greenComponent.settings.addressType
}),
source: quack.ComponentID.create({
type: pressureComponent.type(),
address: pressureComponent.settings.address,
addressType: pressureComponent.settings.addressType
}),
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: -highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
schedule: pond.InteractionSchedule.create({
timezone: moment.tz.guess(),
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
timeOfDayStart: "00:00",
timeOfDayEnd: "24:00"
}),
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
value: 1
})
});
//TOGGLE red OFF when pressure within range of upper and lower
let redToggle: pond.InteractionSettings = pond.InteractionSettings.create({
sink: quack.ComponentID.create({
type: redComponent.type(),
address: redComponent.settings.address,
addressType: redComponent.settings.addressType
}),
source: quack.ComponentID.create({
type: pressureComponent.type(),
address: pressureComponent.settings.address,
addressType: pressureComponent.settings.addressType
}),
conditions: [
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN,
value: -highDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
}),
pond.InteractionCondition.create({
comparison: quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN,
value: -lowDelta,
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE
})
],
nodeOne: 2,
nodeTwo: 1,
subtype: 18,
schedule: pond.InteractionSchedule.create({
timezone: moment.tz.guess(),
weekdays: ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"],
timeOfDayStart: "00:00",
timeOfDayEnd: "24:00"
}),
result: pond.InteractionResult.create({
type: quack.InteractionResultType.INTERACTION_RESULT_TYPE_TOGGLE,
value: 0
})
});
let deviceID = Device.any(compDevice.device).id();
if (deviceID !== undefined) {
let multi = pond.MultiInteractionSettings.create({
interactions: [greenToggle, redToggle]
});
interactionsAPI
.addMultiInteractions(deviceID, multi)
.then(resp => {
openSnack("Interactions added");
})
.catch(err => {
openSnack("There was a problem adding interactions to the device");
})
.finally(() => {
setAdding(false);
});
}
}
close(false);
};
return (
<ResponsiveDialog
open={open}
onClose={() => {
close(true);
}}>
<DialogTitle>Set Interaction For Light Toggle</DialogTitle>
<DialogContent>
Your Delta Pressures, in pascals, will be:
<Typography>low = {lowDelta}</Typography>
<Typography>high = {highDelta}</Typography>
<Typography>{greenComponent ? "" : "Green LED Component not found"}</Typography>
<Typography>{redComponent ? "" : "Red LED Component not found"}</Typography>
</DialogContent>
<DialogActions>
<Button
variant="contained"
color="primary"
onClick={() => {
//TODO: Once we figure out how to fix the backend to handle rapid addition/removal of interactions
//removeCurrentInteractions();
setAdding(true);
createInteractions();
}}
disabled={greenComponent === undefined || redComponent === undefined || adding}>
{adding ? "Adding Interaction" : "Create Interaction"}
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

437
src/gate/GateFlowGraph.tsx Normal file
View file

@ -0,0 +1,437 @@
import {
Box,
Button,
Card,
CardHeader,
CircularProgress,
Grid2 as Grid,
LinearProgress,
Typography
} from "@mui/material";
import { teal } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import SingleSetAreaChart, { SSAreaDataPoint } from "charts/SingleSetAreaChart";
import { Gate } from "models/Gate";
import moment, { Moment } from "moment";
import { pond } from "protobuf-ts/pond";
import { useGateAPI } from "providers";
import React, { useEffect, useState } from "react";
interface Props {
gate: Gate;
device: string | number;
start: Moment;
end: Moment;
ambient?: string;
newXDomain?: number[] | string[];
pressureComponent?: string;
setPCAState: (state: boolean) => void;
multiGraphZoom?: (domain: number[] | string[]) => void;
}
const useStyles = makeStyles(() => ({
calcCard: {
height: 100,
display: "flex",
alignItems: "center"
},
eventCard: {
height: 100,
paddingX: 10,
display: "flex",
alignItems: "center"
},
runtimeGrid: {
marginBottom: 2
},
eventGrid: {
marginTop: 2
}
}));
export default function GateFlowGraph(props: Props) {
const {
gate,
device,
ambient,
pressureComponent,
setPCAState,
start,
end,
newXDomain,
multiGraphZoom
} = props;
const gateAPI = useGateAPI();
const [flowData, setFlowData] = useState<SSAreaDataPoint[]>([]);
const [loadingChartData, setLoadingChartData] = useState(false);
const [recent, setRecent] = useState<SSAreaDataPoint | undefined>();
const [runtime, setRuntime] = useState<moment.Duration>();
const classes = useStyles();
const [flowEvents, setFlowEvents] = useState<pond.AirFlowEvent[]>([]);
const [eventsLoading, setEventsLoading] = useState(false);
const eventThreshold = 5;
const idleFlow = 2.44;
useEffect(() => {
if (loadingChartData) return;
if (ambient && pressureComponent) {
let recent: SSAreaDataPoint | undefined;
setLoadingChartData(true);
gateAPI
.listGateAirflow(
gate.key,
device,
ambient,
pressureComponent,
start.toISOString(),
end.toISOString()
)
.then(resp => {
let data: SSAreaDataPoint[] = [];
if (resp.data.values) {
let start: Moment | undefined;
let stop: Moment | undefined;
let runtime = 0;
resp.data.values.forEach((val, i) => {
let time = moment(val.time);
let newPoint: SSAreaDataPoint = {
timestamp: time.valueOf(),
value: val.airFlow ?? 0
};
data.push(newPoint);
if (!recent || recent.timestamp < newPoint.timestamp) {
recent = newPoint;
}
/** determine runtime */
// set the start time if the values is greater than the idleFlow and start is not already set
if (val.airFlow >= idleFlow && !start) {
start = time;
}
// set the stop time when off or at the last measurements and the start time is set
if ((val.airFlow < idleFlow || i === resp.data.values.length - idleFlow) && start) {
stop = time;
}
// if both start and stop are set calculate add the timeframe to the total runtime
if (start && stop) {
runtime = runtime + stop.diff(start);
start = undefined;
stop = undefined;
}
});
setRuntime(moment.duration(runtime));
let state = false;
if (
data[data.length - 1].value > gate.lowerFlow() &&
data[data.length - 1].value < gate.upperFlow()
) {
state = true;
}
setPCAState(state);
}
setFlowData(data);
setLoadingChartData(false);
setRecent(recent);
});
}
}, [gateAPI, gate, ambient, pressureComponent, start, end, device, setPCAState]); // eslint-disable-line react-hooks/exhaustive-deps
const loadFlowEvents = () => {
if (ambient && pressureComponent) {
setEventsLoading(true);
gateAPI
.listGateFlowEvents(
gate.key,
device,
ambient,
pressureComponent,
start.toISOString(),
end.toISOString(),
idleFlow,
eventThreshold
)
.then(resp => {
console.log(resp);
setFlowEvents(resp.data.events.map(e => pond.AirFlowEvent.fromObject(e)));
})
.catch(err => {})
.finally(() => {
setEventsLoading(false);
});
}
};
const flowChart = () => {
return (
<Card raised style={{ padding: 10, marginBottom: 15 }}>
<CardHeader
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>Mass Air Flow</Typography>}
subheader={
recent ? (
<Grid container>
<Grid size={12}>
<span>
{"Mass Flow Rate: "}
<span style={{ color: teal[500], fontWeight: 500 }}>
{recent.value.toFixed(2)} kg/s
</span>
<br />
</span>
</Grid>
<Grid size={12}>
<Typography color="textSecondary" variant={"caption"}>
{moment(recent.timestamp).fromNow()}
</Typography>
</Grid>
</Grid>
) : (
<Typography variant="body1" color="textPrimary">
No Data
</Typography>
)
}
/>
{flowData.length !== 0 ? (
<SingleSetAreaChart
data={flowData}
maxRef={gate.upperFlow()}
minRef={gate.lowerFlow()}
newXDomain={newXDomain}
multiGraphZoom={multiGraphZoom}
/>
) : (
<Box display="flex" flexDirection="column" flexGrow="2">
<div style={{ display: "flex", flexGrow: 2 }}></div>
<Typography variant="subtitle1" color="textSecondary" align="center">
A component may be missing or have no measurements, this data needs the ambient and
pressure components to be set and measuring
</Typography>
<div style={{ display: "flex", flexGrow: 2 }}></div>
</Box>
)}
</Card>
);
};
const eventCards = () => {
let totalEvents = 0;
let eventsInside = 0;
let eventsOutside = 0;
let totalTimeS = 0;
let timeSInside = 0;
let timeSOutside = 0;
flowEvents.forEach(event => {
totalEvents++;
totalTimeS =
totalTimeS + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
let avg = 0;
let count = 0;
event.readings.forEach(reading => {
avg = avg + reading.airFlow;
count++;
});
avg = avg / count;
//if the average of the readings for an event are within the range
if (avg < gate.upperFlow() && avg > gate.lowerFlow()) {
eventsInside++;
timeSInside =
timeSInside + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
} else {
eventsOutside++;
timeSOutside =
timeSOutside + moment.duration(moment(event.start).diff(moment(event.end))).asSeconds();
}
});
return (
<React.Fragment>
{flowEvents.length > 0 ? (
<Grid container direction="row" spacing={2} className={classes.eventGrid}>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container alignItems="center">
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Total Events:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{totalEvents}
</Typography>
</Box>
</Grid>
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Total Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(totalTimeS, "s").humanize()}
</Typography>
</Box>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container alignItems="center">
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Events Inside:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{eventsInside}
</Typography>
</Box>
</Grid>
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Event Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(timeSInside, "s").humanize()}
</Typography>
</Box>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container alignItems="center">
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Events Outside:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{eventsOutside}
</Typography>
</Box>
</Grid>
<Grid size={9}>
<Box display="flex" justifyContent="space-between">
<Typography>Event Time:</Typography>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{moment.duration(timeSOutside, "s").humanize()}
</Typography>
</Box>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.eventCard}>
<Grid container direction="column" alignItems="center">
<Grid>
<Typography>Performance:</Typography>
</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{(eventsInside / totalEvents) * 100}%
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
</Grid>
) : (
<Box>
{eventsLoading ? (
<Box display="flex" justifyContent="center">
<CircularProgress size={100} />
</Box>
) : (
<Box display="flex" justifyContent="center">
<Button
variant="contained"
color="primary"
onClick={() => {
loadFlowEvents();
}}>
Retrieve Flow Events
</Button>
</Box>
)}
</Box>
)}
</React.Fragment>
);
};
return (
<React.Fragment>
{runtime && (
<Grid container direction="row" spacing={2} className={classes.runtimeGrid}>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Approximate Runtime:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
{runtime.asHours().toFixed(2)} hr
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Cost to run PCA:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(runtime.asHours() * gate.settings.hourlyPcaCost).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Cost to run APU:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(runtime.asHours() * gate.settings.hourlyApuCost).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
<Grid size={3}>
<Card raised className={classes.calcCard}>
<Grid container direction="column" alignItems="center">
<Grid>Total Cost:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(
runtime.asHours() * gate.settings.hourlyApuCost +
runtime.asHours() * gate.settings.hourlyPcaCost
).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
<Grid>Estimated Savings:</Grid>
<Grid>
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
$
{parseFloat(
(runtime.asHours() * gate.settings.hourlyApuCost).toFixed(2)
).toLocaleString()}
</Typography>
</Grid>
</Grid>
</Card>
</Grid>
</Grid>
)}
{loadingChartData ? <LinearProgress /> : flowChart()}
{eventCards()}
</React.Fragment>
);
}

357
src/gate/GateGraphs.tsx Normal file
View file

@ -0,0 +1,357 @@
import {
Avatar,
Box,
Button,
Card,
CardHeader,
LinearProgress,
Theme,
Tooltip,
Typography
} from "@mui/material";
import { ZoomOut } from "@mui/icons-material";
import AreaGraph, { AreaData } from "charts/measurementCharts/AreaGraph";
import MultiLineGraph, { LineData, Point } from "charts/measurementCharts/MultiLineGraph";
import { GetDefaultDateRange } from "common/time/DateRange";
import TimeBar from "common/time/TimeBar";
import UnitMeasurementSummary from "component/UnitMeasurementSummary";
import { useThemeType } from "hooks";
import { Component } from "models";
import { Gate } from "models/Gate";
import { UnitMeasurement } from "models/UnitMeasurement";
import moment, { Moment } from "moment";
import { GetComponentIcon } from "pbHelpers/ComponentType";
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
import { useGateAPI, useGlobalState } from "providers";
import React, { useEffect, useState } from "react";
import { avg } from "utils";
import GateFlowGraph from "./GateFlowGraph";
import { makeStyles } from "@mui/styles";
interface Props {
gate: Gate;
display: "analytics" | "sensors";
compMap: Map<string, Component>;
device: string | number;
pressure: string;
ambient: string;
setPCAState: (state: boolean) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
card: {
position: "relative",
display: "flex",
height: "100%",
flexDirection: "column",
overflow: "visible"
},
cardHeader: {
padding: theme.spacing(1),
paddingLeft: theme.spacing(2),
marginRight: 10
},
avatarIcon: {
width: 33,
height: 33
}
})
);
export default function GateGraphs(props: Props) {
const { gate, display, compMap, device, pressure, ambient, setPCAState } = props;
const [xDomain, setXDomain] = useState<string[] | number[]>(["dataMin", "dataMax"]);
const [zoomed, setZoomed] = useState(false);
const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState<Moment>(defaultDateRange.start);
const [endDate, setEndDate] = useState<Moment>(defaultDateRange.end);
const themeType = useThemeType();
const classes = useStyles();
const [{ user }] = useGlobalState();
const [compMeasurements, setCompMeasurements] = useState<Map<string, UnitMeasurement[]>>(
new Map<string, UnitMeasurement[]>()
);
const gateAPI = useGateAPI();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (loading) return;
setLoading(true);
let measurementMap: Map<string, UnitMeasurement[]> = new Map<string, UnitMeasurement[]>();
gateAPI
.listGateMeasurements(gate.key, startDate.toISOString(), endDate.toISOString())
.then(resp => {
resp.data.measurements.forEach(um => {
let unitMeasurement = UnitMeasurement.any(um, user);
let entry = measurementMap.get(unitMeasurement.componentId);
if (entry) {
entry.push(unitMeasurement);
} else {
measurementMap.set(unitMeasurement.componentId, [unitMeasurement]);
}
});
setLoading(false);
setCompMeasurements(measurementMap);
});
}, [gate, endDate, gateAPI, startDate]); // eslint-disable-line react-hooks/exhaustive-deps
const updateDateRange = (newStartDate: any, newEndDate: any) => {
let range = GetDefaultDateRange();
range.start = newStartDate;
range.end = newEndDate;
setStartDate(newStartDate);
setEndDate(newEndDate);
};
const zoomOut = () => {
setXDomain(["dataMin", "dataMax"]);
setZoomed(false);
};
const lineGraph = (
unitMeasurement: UnitMeasurement,
describer: MeasurementDescriber,
averages?: number
) => {
if (unitMeasurement.values.length > 0) {
let firstTime: Moment | undefined;
let lastTime: Moment | undefined;
//build line data to pass to graph
let lineData: LineData[] = [];
let averagedData: LineData[] = [];
let valMap: Map<number, number[]> = new Map<number, number[]>();
unitMeasurement.values.forEach((vals, i) => {
let avgVals: Point[] = [];
let newLineData: LineData = {
timestamp: moment(unitMeasurement.timestamps[i]).valueOf(),
points: vals.values.map((val, i) => ({ value: val, node: i }))
};
lineData.push(newLineData);
if (averages) {
vals.values.forEach((val, k) => {
let entry = valMap.get(k);
if (entry) {
entry.push(val);
if (entry.length >= averages) {
lastTime = moment(unitMeasurement.timestamps[i]);
avgVals.push({
node: k,
value: avg(entry)
});
}
} else {
valMap.set(k, [val]);
firstTime = moment(unitMeasurement.timestamps[i]);
}
});
}
if (firstTime && lastTime) {
averagedData.push({
points: avgVals,
timestamp:
Math.abs(firstTime.diff(lastTime)) / 2 +
Math.min(firstTime.valueOf(), lastTime.valueOf())
});
firstTime = undefined;
lastTime = undefined;
valMap = new Map<number, number[]>();
}
});
return (
<MultiLineGraph
customHeight={300}
key={unitMeasurement.type}
lineData={lineData}
//averagedData={showAveraged ? averagedData : []}
numLines={unitMeasurement.values.length > 1 ? unitMeasurement.values[0].values.length : 0}
describer={describer}
tooltip
newXDomain={xDomain}
multiGraphZoom={domain => {
setXDomain(domain);
setZoomed(true);
}}
multiGraphZoomOut
/>
);
}
};
const areaGraph = (
unitMeasurement: UnitMeasurement,
describer: MeasurementDescriber,
averages?: number
) => {
if (unitMeasurement.timestamps.length > 0) {
let firstTime: Moment | undefined;
let lastTime: Moment | undefined;
//build line data to pass to graph
let areaData: AreaData[] = [];
let avgData: AreaData[] = [];
let maxVals: number[] = [];
let minVals: number[] = [];
unitMeasurement.values.forEach((val, j) => {
let currentMax = Math.min(...val.values);
let currentMin = Math.max(...val.values);
let dataPoint: AreaData = {
timestamp: moment(unitMeasurement.timestamps[j]).valueOf(),
value: [currentMax, currentMin]
};
if (averages) {
if (minVals.length === 0) {
firstTime = moment(unitMeasurement.timestamps[j]);
}
maxVals.push(currentMax);
minVals.push(currentMin);
if (minVals.length >= averages) {
lastTime = moment(unitMeasurement.timestamps[j]);
}
}
if (firstTime && lastTime) {
avgData.push({
timestamp:
Math.abs(firstTime.diff(lastTime)) / 2 +
Math.min(firstTime.valueOf(), lastTime.valueOf()),
value: [avg(maxVals), avg(minVals)]
});
firstTime = undefined;
lastTime = undefined;
maxVals = [];
minVals = [];
}
areaData.push(dataPoint);
});
return (
<AreaGraph
key={unitMeasurement.type}
customHeight={350}
data={areaData}
//averagedData={showAveraged ? avgData : []}
describer={describer}
tooltip
newXDomain={xDomain}
multiGraphZoom={domain => {
setXDomain(domain);
setZoomed(true);
}}
multiGraphZoomOut
/>
);
}
};
const graphHeader = (comp: Component) => {
const componentIcon = GetComponentIcon(comp.settings.type, comp.settings.subtype, themeType);
return (
<CardHeader
avatar={
<Avatar
variant="square"
src={componentIcon}
className={classes.avatarIcon}
alt={comp.name()}
/>
}
title={<Typography style={{ fontSize: 25, fontWeight: 650 }}>{comp.name()}</Typography>}
subheader={
<UnitMeasurementSummary
component={comp}
reading={UnitMeasurement.convertLastMeasurement(
comp.status.measurement.map(um => UnitMeasurement.create(um, user)) ?? []
)}
/>
}
/>
);
};
const sensorGraphs = () => {
return (
<Box>
{Array.from(compMeasurements.values()).map((compMeasurement, i) => {
let c = Component.create();
if (compMeasurement[0]) {
c = compMap.get(compMeasurement[0].componentId) ?? Component.create();
}
if (compMap.get(c.key())) {
return (
<Card raised key={i} style={{ padding: 10, marginBottom: 15 }}>
{graphHeader(c)}
{compMeasurement.map(um => {
if (um.values[0] && um.values[0].values.length > 1) {
return areaGraph(
um,
describeMeasurement(um.type, c.type(), c.subType()),
c.settings.smoothingAverages
);
} else {
return lineGraph(
um,
describeMeasurement(um.type, c.type(), c.subType()),
c.settings.smoothingAverages
);
}
})}
</Card>
);
} else {
return <Box></Box>;
}
})}
</Box>
);
};
const analyticGraphs = () => {
return (
<Box>
<GateFlowGraph
newXDomain={xDomain}
device={device}
start={startDate}
end={endDate}
gate={gate}
ambient={ambient}
pressureComponent={pressure}
setPCAState={(state: boolean) => {
setPCAState(state);
}}
multiGraphZoom={domain => {
setXDomain(domain);
setZoomed(true);
}}
/>
</Box>
);
};
const showGraphs = () => {
switch (display) {
case "sensors":
return sensorGraphs();
case "analytics":
return analyticGraphs();
default:
return <Box>Unknown Graph Type Selected</Box>;
}
};
return (
<React.Fragment>
<Box display="flex" justifyContent="space-between" alignItems="center" marginBottom={2}>
<TimeBar updateDateRange={updateDateRange} startDate={startDate} endDate={endDate} />
{zoomed && (
<Tooltip title="Zoom Out Graphs">
<Button variant="outlined" onClick={zoomOut}>
<ZoomOut />
</Button>
</Tooltip>
)}
</Box>
{loading ? <LinearProgress /> : showGraphs()}
</React.Fragment>
);
}

143
src/gate/GateList.tsx Normal file
View file

@ -0,0 +1,143 @@
import { Gate } from "models/Gate";
import { Box, Card, Theme } from "@mui/material";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useMobile } from "hooks";
//import MaterialTable from "material-table";
//import { getTableIcons } from "common/ResponsiveTable";
import { Terminal } from "models/Terminal";
import { makeStyles } from "@mui/styles";
interface Props {
gates: Gate[];
terminals: Terminal[];
useMobile: boolean;
//duplicateGate: (gate: Gate) => void;
}
const useStyles = makeStyles((theme: Theme) => ({
gridListTile: {
minHeight: "184px",
height: "auto !important",
width: "184px",
padding: 2
},
hidden: {
visibility: "hidden"
},
gateCard: {
marginBottom: 10,
paddingLeft: 15
}
}));
export default function GateList(props: Props) {
const { gates, terminals } = props;
// const history = useHistory();
const navigate = useNavigate();
const isMobile = useMobile();
const classes = useStyles();
const [terminalMap, setTerminalMap] = useState<Map<string, string>>(new Map<string, string>());
const goToGate = (gate: Gate) => {
let path = "/terminals/" + gate.key;
navigate(path, { state: gate })
};
useEffect(() => {
let map = new Map<string, string>();
terminals.forEach(t => {
map.set(t.key, t.name);
});
setTerminalMap(map);
}, [terminals]);
const desktopView = () => {
return (<Box>Desktop table</Box>)
// return (
// <MaterialTable
// columns={[
// {
// title: "Gate",
// field: "name",
// headerStyle: {
// fontWeight: 650,
// fontSize: 20
// }
// },
// {
// title: "Terminals",
// field: "settings.terminal",
// headerStyle: {
// fontWeight: 650,
// fontSize: 20
// },
// render: rowData => terminalMap.get(rowData.terminal())
// },
// {
// title: "Duct Type",
// field: "settings.ductName",
// headerStyle: {
// fontWeight: 650,
// fontSize: 20
// }
// },
// {
// title: "Duct Size(mm)",
// field: "settings.ductDiameter",
// headerStyle: {
// fontWeight: 650,
// fontSize: 20
// }
// },
// {
// title: "Duct Length(m)",
// field: "settings.ductLength",
// headerStyle: {
// fontWeight: 650,
// fontSize: 20
// }
// },
// {
// title: "PCA Unit",
// field: "settings.pcaType",
// headerStyle: {
// fontWeight: 650,
// fontSize: 20
// }
// }
// ]}
// data={gates}
// icons={getTableIcons()}
// onRowClick={(_, gate) => {
// gate && goToGate(gate);
// }}
// title={""}
// options={{
// pageSize: 10
// }}
// />
// );
};
const mobileView = () => {
return (
<React.Fragment>
{gates.map((gate, i) => (
<Card
key={i}
className={classes.gateCard}
onClick={() => {
goToGate(gate);
}}>
<Box margin={2} fontSize={20} fontWeight={650}>
{gate.name}
</Box>
</Card>
))}
</React.Fragment>
);
};
return isMobile || props.useMobile ? mobileView() : desktopView();
}

374
src/gate/GateSVG.tsx Normal file

File diff suppressed because one or more lines are too long

574
src/gate/GateSettings.tsx Normal file
View file

@ -0,0 +1,574 @@
import {
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
Select,
TextField,
Button,
Grid2 as Grid,
Box,
InputAdornment,
Stepper,
Step,
StepLabel,
Tabs,
Tab
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Terminal } from "models/Terminal";
import { Gate } from "models/Gate";
import { pond } from "protobuf-ts/pond";
import { useTerminalAPI, useGateAPI } from "providers";
import React, { useEffect, useState } from "react";
import { useSnackbar } from "hooks";
//import { useHistory } from "react-router";
import { useNavigate } from "react-router-dom";
import DuctDescriber, { DuctOptions, FindDuctType } from "ducting/DuctDescriber";
interface DuctProps {
diameter: number;
length: number;
thermalConductivity: number;
thermalResistance: number;
frictionFactor: number;
}
interface Props {
open: boolean;
close: (newGate?: Gate) => void;
terminals?: Terminal[];
gate?: Gate;
long?: number;
lat?: number;
}
const steps = ["Gate", "Duct"];
export default function GateSettings(props: Props) {
const { open, close, terminals, gate, long, lat } = props;
const [gateName, setGateName] = useState("");
const [lowerFlowBound, setLowerFlowBound] = useState(0);
const [upperFlowBound, setUpperFlowBound] = useState(0);
const [terminalOptions, setTerminalOptions] = useState(terminals);
const [terminalKey, setTerminalKey] = useState("");
const gateAPI = useGateAPI();
const terminalAPI = useTerminalAPI();
const [loadingTerminals, setLoadingTerminals] = useState(false);
const [removeDialog, setRemoveDialog] = useState(false);
const { openSnack } = useSnackbar();
//const history = useHistory();
const navigate = useNavigate();
const ductOptions = DuctOptions();
const [ductType, setDuctType] = useState(0);
const [ductName, setDuctName] = useState("");
const [ductProps, setDuctProps] = useState<DuctProps>({
diameter: 0,
length: 0,
frictionFactor: 0,
thermalConductivity: 0,
thermalResistance: 0
});
const [pcaUnit, setPcaUnit] = useState("");
const [activeStep, setActiveStep] = useState(0);
//user set identifier to be shown on the marker on the map
const [terminalIdentifier, setTerminalIdentifier] = useState("");
const [gateIdentifier, setGateIdentifier] = useState("");
const [hourlyPCA, setHourlyPCA] = useState<number>(0);
const [hourlyAPU, setHourlyAPU] = useState<number>(0);
useEffect(() => {
if (!terminals) {
if (loadingTerminals) return;
setLoadingTerminals(true);
terminalAPI
.listTerminals(500, 0)
.then(resp => {
setTerminalOptions(resp.data.terminals.map(a => Terminal.any(a)));
})
.catch(err => {})
.finally(() => {
setLoadingTerminals(false);
});
} else {
setTerminalOptions(terminals);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [terminals, terminalAPI]);
useEffect(() => {
if (gate) {
setGateName(gate.name);
setTerminalKey(gate.terminal());
setDuctProps({
diameter: gate.settings.ductDiameter,
length: gate.settings.ductLength,
frictionFactor: gate.settings.frictionFactor,
thermalConductivity: gate.settings.thermalConductivity,
thermalResistance: gate.settings.thermalResistance
});
let currentType = FindDuctType(
gate.settings.thermalResistance,
gate.settings.thermalConductivity,
gate.settings.frictionFactor
);
setDuctType(currentType);
setLowerFlowBound(gate.lowerFlow());
setUpperFlowBound(gate.upperFlow());
setPcaUnit(gate.settings.pcaType);
setDuctName(gate.settings.ductName);
setTerminalIdentifier(gate.settings.letterIdentifier ?? "");
setGateIdentifier(gate.settings.numberIdentifier ?? "");
setHourlyAPU(gate.settings.hourlyApuCost ?? 0);
setHourlyPCA(gate.settings.hourlyPcaCost ?? 0);
} else {
setGateName("");
setTerminalKey("");
}
}, [gate]);
const confirm = () => {
if (gate) {
let settings = gate.settings;
settings.terminal = terminalKey;
settings.ductDiameter = ductProps.diameter;
settings.ductLength = ductProps.length;
settings.frictionFactor = ductProps.frictionFactor;
settings.thermalConductivity = ductProps.thermalConductivity;
settings.thermalResistance = ductProps.thermalResistance;
settings.lowerFlow = lowerFlowBound;
settings.upperFlow = upperFlowBound;
settings.ductName = ductName;
settings.pcaType = pcaUnit;
settings.letterIdentifier = terminalIdentifier;
settings.numberIdentifier = gateIdentifier.toString();
settings.hourlyApuCost = hourlyAPU;
settings.hourlyPcaCost = hourlyPCA;
gateAPI
.updateGate(gate.key, gateName, settings)
.then(resp => {
openSnack("Gate update");
close();
})
.catch(err => {
openSnack("Failed to update gate");
});
} else {
let settings: pond.GateSettings = pond.GateSettings.create({
longitude: long,
latitude: lat,
terminal: terminalKey,
ductDiameter: ductProps.diameter,
ductLength: ductProps.length,
frictionFactor: ductProps.frictionFactor,
thermalConductivity: ductProps.thermalConductivity,
thermalResistance: ductProps.thermalResistance,
lowerFlow: lowerFlowBound,
upperFlow: upperFlowBound,
ductName: ductName,
pcaType: pcaUnit,
letterIdentifier: terminalIdentifier,
numberIdentifier: gateIdentifier.toString(),
hourlyApuCost: hourlyAPU,
hourlyPcaCost: hourlyPCA
});
gateAPI
.addGate(gateName, settings)
.then(resp => {
openSnack("New gate added");
let newGate = Gate.create(
pond.Gate.create({ key: resp.data.key, name: gateName, settings: settings })
);
close(newGate);
})
.catch(err => {
openSnack("There was a problem adding your gate");
});
}
};
const remove = () => {
if (gate) {
gateAPI
.removeGate(gate.key)
.then(resp => {
openSnack("Gate Deleted");
})
.catch(err => {
openSnack("Failed to delete gate");
})
.finally(() => {
navigate("/terminals");
});
}
};
const stepper = () => {
if (gate === undefined) {
return (
<Stepper activeStep={activeStep}>
{steps.map(label => {
const stepProps: { completed?: boolean } = {};
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
);
}
return (
<Box display="flex" justifyContent="center" width="100%">
<Tabs
value={activeStep}
onChange={(_, value) => setActiveStep(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
aria-label="bin tabs"
//classes={{ root: classes.tabs }}
>
{steps.map((step, i) => (
<Tab key={i} label={step} aria-label={step} />
))}
</Tabs>
</Box>
);
};
const stepperContent = (step: number) => {
switch (step) {
case 1:
return ductProperties();
default:
return gateProperties();
}
};
const gateProperties = () => {
//build list of letters for select box
let tiOptions: JSX.Element[] = [];
//adds the letters to the terminal id options
for (let l = 65; l <= 90; l++) {
tiOptions.push(
<MenuItem key={String.fromCharCode(l)} value={String.fromCharCode(l)}>
{String.fromCharCode(l)}
</MenuItem>
);
}
//add numbers 1-9 for terminal id options
for (let n = 1; n <= 9; n++) {
tiOptions.push(
<MenuItem key={n} value={n}>
{n}
</MenuItem>
);
}
let numberOptions: JSX.Element[] = [];
for (let n = 0; n <= 99; n++) {
numberOptions.push(
<MenuItem key={n} value={n}>
{n}
</MenuItem>
);
}
return (
<React.Fragment>
<TextField
label="Gate Name"
fullWidth
value={gateName}
onChange={e => setGateName(e.target.value)}
/>
<Select
id="terminal"
label="Terminal"
fullWidth
displayEmpty
value={terminalKey}
onChange={e => {
setTerminalKey(e.target.value as string);
}}>
<MenuItem key="new" value="">
Select Terminal
</MenuItem>
{terminalOptions?.map(terminal => (
<MenuItem key={terminal.key} value={terminal.key}>
{terminal.name}
</MenuItem>
))}
</Select>
<TextField
label="Low Mass Air Flow"
fullWidth
type="number"
value={lowerFlowBound}
onChange={e => setLowerFlowBound(+e.target.value)}
/>
<TextField
label="High Mass Air Flow"
fullWidth
type="number"
value={upperFlowBound}
onChange={e => setUpperFlowBound(+e.target.value)}
/>
<TextField
label="PCA Unit"
fullWidth
value={pcaUnit}
onChange={e => setPcaUnit(e.target.value)}
/>
<TextField
style={{ width: "45%" }}
select
label="Terminal Identifier"
value={terminalIdentifier}
onChange={e => {
setTerminalIdentifier(e.target.value as string);
}}>
<MenuItem key="new" value="">
Gate Letter
</MenuItem>
{tiOptions}
</TextField>
<TextField
style={{ width: "45%", marginLeft: "10%" }}
label="Gate Identifier"
select
value={gateIdentifier}
onChange={e => setGateIdentifier(e.target.value)}>
<MenuItem key="new" value="">
Gate Number
</MenuItem>
{numberOptions}
</TextField>
<TextField
label="Hourly PCA Cost"
fullWidth
type="number"
value={hourlyPCA}
InputProps={{
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
}}
onChange={e => setHourlyPCA(+e.target.value)}
/>
<TextField
label="Hourly APU Cost"
fullWidth
type="number"
value={hourlyAPU}
InputProps={{
endAdornment: <InputAdornment position="end">$/hr</InputAdornment>
}}
onChange={e => setHourlyAPU(+e.target.value)}
/>
</React.Fragment>
);
};
const ductProperties = () => {
return (
<React.Fragment>
<TextField
id="ductType"
label="Duct Type"
fullWidth
value={ductType}
select
onChange={e => {
let type = +e.target.value;
setDuctType(type);
setDuctName("custom");
if (type > 1) {
let describer = DuctDescriber(type);
setDuctName(describer.name);
setDuctProps({
diameter: ductProps.diameter,
frictionFactor: describer.frictionFactor,
length: ductProps.length,
thermalConductivity: describer.thermalConductivity,
thermalResistance: describer.thermalResistance
});
}
}}>
<MenuItem key="new" value={pond.DuctType.DUCT_TYPE_INVALID}>
Select Ducting Type
</MenuItem>
{ductOptions.map(duct => (
<MenuItem key={duct.value} value={duct.value}>
{duct.label}
</MenuItem>
))}
<MenuItem key="custom" value={pond.DuctType.DUCT_TYPE_NONE}>
Custom
</MenuItem>
</TextField>
<TextField
margin="normal"
id="ductDiameter"
label="Duct Diameter(mm)"
fullWidth
type="text"
value={isNaN(ductProps.diameter) ? "" : ductProps.diameter}
error={isNaN(+ductProps.diameter)}
InputProps={{
endAdornment: <InputAdornment position="end">mm</InputAdornment>
}}
onChange={e => {
setDuctProps({ ...ductProps, diameter: parseFloat(e.target.value) });
}}
/>
<TextField
margin="normal"
id="ductLength"
label="Duct Length(m)"
fullWidth
type="text"
value={isNaN(ductProps.length) ? "" : ductProps.length}
error={isNaN(+ductProps.length)}
InputProps={{
endAdornment: <InputAdornment position="end">m</InputAdornment>
}}
onChange={e => {
setDuctProps({ ...ductProps, length: parseFloat(e.target.value) });
}}
/>
<TextField
margin="normal"
id="thermalCond"
label="Thermal Conductivity"
fullWidth
type="text"
value={isNaN(ductProps.thermalConductivity) ? "" : ductProps.thermalConductivity}
error={isNaN(+ductProps.thermalConductivity)}
InputProps={{
endAdornment: <InputAdornment position="end">W/mK</InputAdornment>
}}
onChange={e => {
setDuctProps({ ...ductProps, thermalConductivity: parseFloat(e.target.value) });
setDuctType(1);
setDuctName("custom");
}}
/>
<TextField
margin="normal"
id="friction"
label="Friction Factor"
fullWidth
type="text"
value={isNaN(ductProps.frictionFactor) ? "" : ductProps.frictionFactor}
error={isNaN(+ductProps.frictionFactor)}
onChange={e => {
setDuctProps({ ...ductProps, frictionFactor: parseFloat(e.target.value) });
setDuctType(1);
setDuctName("custom");
}}
/>
<TextField
margin="normal"
id="thermalRes"
label="Thermal Resistance"
fullWidth
type="text"
value={isNaN(ductProps.thermalResistance) ? "" : ductProps.thermalResistance}
error={isNaN(+ductProps.thermalResistance)}
InputProps={{
endAdornment: <InputAdornment position="end">K/W</InputAdornment>
}}
onChange={e => {
setDuctProps({ ...ductProps, thermalResistance: parseFloat(e.target.value) });
setDuctType(1);
setDuctName("custom");
}}
/>
</React.Fragment>
);
};
const removeConfirmation = () => {
return (
<ResponsiveDialog open={removeDialog} onClose={() => setRemoveDialog(false)}>
<DialogTitle>Delete Gate</DialogTitle>
<DialogContent>Are you sure you wish to delete this gate?</DialogContent>
<DialogActions>
<Button
onClick={() => {
setRemoveDialog(false);
}}>
Cancel
</Button>
<Button onClick={remove}>Confirm</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<ResponsiveDialog open={open} onClose={() => close()}>
{removeConfirmation()}
<DialogTitle></DialogTitle>
<DialogContent>
{stepper()}
{stepperContent(activeStep)}
</DialogContent>
<DialogActions>
<Grid container direction="row">
<Grid>
{gate && (
<Button
variant="contained"
style={{
backgroundColor: "red"
}}
onClick={() => {
setRemoveDialog(true);
}}>
Delete
</Button>
)}
</Grid>
<Grid>
{(gate || activeStep === 0) && (
<Button
onClick={() => {
close();
}}>
Cancel
</Button>
)}
{!gate && activeStep > 0 && (
<Button
onClick={() => {
setActiveStep(activeStep - 1);
}}>
Back
</Button>
)}
{(gate || activeStep === 1) && (
<Button variant="contained" color="primary" onClick={confirm}>
Confirm
</Button>
)}
{!gate && activeStep === 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
setActiveStep(activeStep + 1);
}}>
Next
</Button>
)}
</Grid>
</Grid>
</DialogActions>
</ResponsiveDialog>
);
}

99
src/models/Gate.ts Normal file
View file

@ -0,0 +1,99 @@
import { cloneDeep } from "lodash";
import { MarkerData } from "maps/mapMarkers/Markers";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
export class Gate {
public settings: pond.GateSettings = pond.GateSettings.create();
public name: string = "Gate";
public key: string = "";
public preferences: any = {};
public gateMutations: any = {};
public pcaState: pond.PCAState = pond.PCAState.PCA_STATE_UNKNOWN;
public static create(pb?: pond.Gate): Gate {
let my = new Gate();
if (pb) {
let g = pond.Gate.fromObject(pb);
my.settings = pond.GateSettings.fromObject(cloneDeep(or(g.settings, {})));
my.name = g.name;
my.key = g.key;
my.preferences = g.componentPreferences;
my.gateMutations = g.gateMutations;
my.pcaState = g.pcaState;
}
return my;
}
public static clone(other?: Gate): Gate {
if (other) {
return Gate.create(
pond.Gate.fromObject({
name: other.name,
key: other.key,
preferences: other.preferences,
gateMutations: other.gateMutations,
settings: cloneDeep(other.settings)
})
);
}
return Gate.create();
}
public static any(data: any): Gate {
return Gate.create(pond.Gate.fromObject(cloneDeep(data)));
}
public longitude(): number {
return this.settings.longitude;
}
public latitude(): number {
return this.settings.latitude;
}
public terminal(): string {
return this.settings.terminal;
}
public upperFlow(): number {
return this.settings.upperFlow;
}
public lowerFlow(): number {
return this.settings.lowerFlow;
}
public ductDiameter(): number {
return this.settings.ductDiameter;
}
public gateMarkerColour(): string {
switch (this.pcaState) {
case pond.PCAState.PCA_STATE_OFF:
return "grey";
case pond.PCAState.PCA_STATE_IN_BOUNDS:
return "green";
case pond.PCAState.PCA_STATE_OUT_BOUNDS:
return "red";
default:
return "#337acc";
}
}
public getMarkerData(
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void,
updateFunc?: (location: pond.Location) => void
): MarkerData {
let m: MarkerData = {
longitude: this.longitude(),
latitude: this.latitude(),
title: this.name,
colour: this.gateMarkerColour(),
visibleLevels: { min: 15 },
clickFunc: clickFunc,
updateFunc: updateFunc
};
return m;
}
}

59
src/models/Terminal.ts Normal file
View file

@ -0,0 +1,59 @@
import { cloneDeep } from "lodash";
import { MarkerData } from "maps/mapMarkers/Markers";
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
export class Terminal {
public settings: pond.TerminalSettings = pond.TerminalSettings.create();
public name: string = "";
public key: string = "";
public static create(pb?: pond.Terminal): Terminal {
let my = new Terminal();
if (pb) {
my.settings = pond.TerminalSettings.fromObject(cloneDeep(or(pb.settings, {})));
my.name = pb.name;
my.key = pb.key;
}
return my;
}
public static clone(other?: Terminal): Terminal {
if (other) {
return Terminal.create(
pond.Terminal.fromObject({
settings: cloneDeep(other.settings)
})
);
}
return Terminal.create();
}
public static any(data: any): Terminal {
return Terminal.create(pond.Terminal.fromObject(cloneDeep(data)));
}
public longitude(): number {
return this.settings.longitude;
}
public latitude(): number {
return this.settings.latitude;
}
public getMarkerData(
clickFunc?: (event: React.PointerEvent<HTMLElement>, index: number, isMobile: boolean) => void,
updateFunc?: (location: pond.Location) => void
): MarkerData {
let m: MarkerData = {
longitude: this.longitude(),
latitude: this.latitude(),
title: this.name,
colour: this.settings.theme?.color ?? "#004f9b",
visibleLevels: { max: 15 },
clickFunc: clickFunc,
updateFunc: updateFunc
};
return m;
}
}

View file

@ -24,3 +24,4 @@ export * from "./Note";
export * from "./FieldMarker";
export * from "./GrainBag";
export * from "./Contract";
export * from "./Terminal";

View file

@ -9,6 +9,8 @@ import { getWhitelabel } from "services/whiteLabel";
import Ventilation from "pages/VentEditor";
import FieldMap from "pages/FieldMap";
import GrainBag from "pages/grainBag";
import Terminals from "pages/Terminals";
import Gate from "pages/Gate";
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
const DevicePage = lazy(() => import("pages/Device"));
@ -46,6 +48,7 @@ export default function Router(props: Props) {
<Route path="groups/*" element={<GroupsRoute/>} />
<Route path="bins/*" element={<BinsRoute/>} />
<Route path="mines/*" element={<MinesRoute/>} />
<Route path="terminals/*" element={<TerminalGatesRoute />} />
</Routes>
)
}
@ -151,6 +154,28 @@ export default function Router(props: Props) {
);
};
const TerminalGatesRoute = () => {
return (
<div>
<Routes>
<Route
key="Terminal page route"
path="" // "/settings/basic"
element={<Terminals key={"Mines page"} />}
/>
<Route
path="/:gateKey" // "/settings/basic"
element={<Gate />}
/>
<Route
path="/:gateKey/*" // "/settings/basic"
element={<RelativeRoutes />}
/>
</Routes>
</div>
);
};
const GroupsRoute = () => {
return (

View file

@ -30,11 +30,13 @@ import {
// IsAdCon,
// isBXT,
IsMiVent,
IsOmniAir,
// IsOmniAir
} from "services/whiteLabel";
import MiningIcon from "products/ventilation/MiningIcon";
import { useAuth0 } from "@auth0/auth0-react";
import FieldsIcon from "products/AgIcons/FieldsIcon";
import PlaneIcon from "products/AviationIcons/PlaneIcon";
const drawerWidth = 230;
@ -140,6 +142,7 @@ export default function SideNavigator(props: Props) {
const authenticatedSideMenu = () => {
const isMiVent = IsMiVent();
const isAg = IsAdaptiveAgriculture()
const isMiPCA = IsOmniAir()
return (
<List className={classes.list} component="nav">
{(isAg || user.hasFeature("admin")) && (
@ -156,6 +159,20 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
)}
{(isMiPCA || user.hasFeature("admin")) && (
<Tooltip title="Terminals" placement="right">
<ListItemButton
id="tour-terminals"
onClick={() => goTo("/terminals")}
classes={getClasses("/terminal")}
>
<ListItemIcon>
<PlaneIcon />
</ListItemIcon>
{open && <ListItemText primary="Terminals" />}
</ListItemButton>
</Tooltip>
)}
<Tooltip title="Devices" placement="right">
<ListItemButton
id="tour-dashboard"
@ -209,7 +226,7 @@ export default function SideNavigator(props: Props) {
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Visual Farm" placement="right">
<ListItemButton
id="tour-dashboard"
id="tour-visual-farm"
onClick={() => goTo("/visualFarm")}
classes={getClasses("/visualFarm")}
>

View file

@ -166,7 +166,7 @@ export default function Bin(props: Props) {
const classes = useStyles();
// const match = useRouteMatch<MatchParams>();
// const binID = binKey ?? match.params.binID;
const binID = useParams<{ binID: string }>()?.binID ?? "";
const binID = binKey ?? useParams<{ binID: string }>()?.binID ?? "";
const binAPI = useBinAPI();
const [{ user }] = useGlobalState();
const [binLoading, setBinLoading] = useState(false);
@ -269,7 +269,7 @@ export default function Bin(props: Props) {
loadRef.current = true;
//add the presets to the bin page data load
binAPI
.getBinPageData(binKey ?? binID, user.id(), showErrors)
.getBinPageData(binID, user.id(), showErrors)
.then(resp => {
if (resp.data.grainCompositionNames) {
let tempMap: Map<string, string> = new Map();
@ -559,7 +559,7 @@ export default function Bin(props: Props) {
const goToDevice = (dev: Device) => {
if (fromMap) {
navigate("/bins/" + binKey + "/devices/" + dev.id(), { replace: true });
navigate("/bins/" + binID + "/devices/" + dev.id(), { replace: true });
} else {
navigate(window.location.pathname + "/devices/" + dev.id(), { replace: true });
}

476
src/pages/Gate.tsx Normal file
View file

@ -0,0 +1,476 @@
import { Gate as IGate } from "models/Gate";
//import { MatchParams } from "navigation/Routes";
//import { Redirect, useHistory, useRouteMatch } from "react-router";
import React, { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer";
import { useGlobalState, useGateAPI, useUserAPI } from "providers";
import {
Box,
ButtonBase,
Card,
CircularProgress,
Drawer,
Grid2 as Grid,
IconButton,
MenuItem,
Tab,
Tabs,
Theme,
Typography
} from "@mui/material";
import { pond } from "protobuf-ts/pond";
import DeviceLinkDrawer from "common/DeviceLinkDrawer";
import { Component, Device, Scope } from "models";
import GateActions from "gate/GateActions";
import GateDevice from "gate/GateDevice";
import ObjectControls from "common/ObjectControls";
import { Link } from "@mui/icons-material";
import Chat from "chat/Chat";
import PlaneIcon from "products/AviationIcons/PlaneIcon";
import NotesIcon from "@mui/icons-material/Notes";
import { useMobile, useSnackbar, useThemeType } from "hooks";
import { clone } from "lodash";
import { useNavigate, useParams } from "react-router-dom";
import { makeStyles } from "@mui/styles";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
interface Props {
gateKey?: string;
useMobile?: boolean;
}
const useStyles = makeStyles((theme: Theme) => ({
//const themeType = theme.palette.type;
inactiveButton: {
color: useThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: "transparent",
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
activeButton: {
color: useThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white,
backgroundColor: theme.palette.primary.main,
width: theme.spacing(5),
height: theme.spacing(5),
border: "1px solid",
borderColor: theme.palette.divider
},
drawerPaper: {
height: "100%",
width: "30%"
}
}));
export default function Gate(props: Props) {
//const match = useRouteMatch<MatchParams>();
//const gateID = props.gateID ?? match.params.gateID;
const { gateKey } = props
const gateID = gateKey ?? useParams<{ gateID: string }>()?.gateID ?? "";
const classes = useStyles();
const gateAPI = useGateAPI();
const userAPI = useUserAPI();
const [gate, setGate] = useState<IGate>(IGate.create());
const [devices, setDevices] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [components, setComponents] = useState<Component[]>([]);
const [openDeviceDrawer, setOpenDeviceDrawer] = useState(false);
const [loadingGate, setLoadingGate] = useState(false);
const [{ user, as }] = useGlobalState();
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [tabVal, setTabVal] = useState(0);
//const history = useHistory();
const navigate = useNavigate();
const [displayTab, setDisplayTab] = useState(0);
const [openNoteDrawer, setOpenNoteDrawer] = useState(false);
const isMobile = useMobile();
const [devPrefs, setDevPrefs] = useState<Map<number, number>>(new Map<number, number>());
const { openSnack } = useSnackbar();
const [invalid, setInvalid] = useState(false);
const goToMap = () => {
navigate("/aviationMap", { state: { long: gate.longitude(), lat:gate.latitude() }})
};
useEffect(() => {
let key = gateID;
let kind = "gate";
if (as) {
key = as;
kind = "team";
}
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
setPermissions(resp.permissions);
});
}, [as, gateID, userAPI, user]);
const loadGate = useCallback(() => {
let id = gateID;
setTabVal(0);
if (loadingGate || id === undefined || id === "") return;
setLoadingGate(true);
gateAPI
.getGatePageData(id)
.then(resp => {
//console.log(resp.data);
let p = new Map<number, pond.GateDeviceType>();
Object.keys(resp.data.preferences).forEach(k => {
let prefKey = parseInt(k);
p.set(
prefKey,
pond.GatePreferences.fromObject(resp.data.preferences[k]).gateDevice ??
pond.GateDeviceType.GATE_DEVICE_TYPE_UNKNOWN
);
});
setDevPrefs(p);
if (resp.data.gate) {
setGate(IGate.any(resp.data.gate));
}
if (resp.data.linkedDevices) {
let devMap = new Map<string, pond.ComprehensiveDevice>();
resp.data.linkedDevices.forEach(dev => {
if (dev.device?.settings?.deviceId) {
devMap.set(dev.device.settings.deviceId.toString(), dev);
}
});
setDevices(devMap);
}
if (resp.data.linkedComponents) {
setComponents(resp.data.linkedComponents.map(c => Component.any(c)));
}
})
.catch(err => {
setInvalid(true);
})
.finally(() => {
setLoadingGate(false);
});
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [gateAPI, gateID]);
useEffect(() => {
loadGate();
}, [loadGate]);
const updateGateDevicePrefs = (deviceID: number, newPref: pond.GateDeviceType) => {
gateAPI
.updatePrefs(gate.key, "device", deviceID.toString(), newPref, [gate.key], ["gate"])
.then(resp => {
openSnack("Updated gate device type");
// have to do the clone method to force the select box to update
let newPrefMap = clone(devPrefs);
newPrefMap.set(deviceID, newPref);
setDevPrefs(newPrefMap);
})
.catch(err => {
openSnack("Failed to update device type");
});
};
const deviceDrawer = () => {
return (
<DeviceLinkDrawer
deviceTags={["omniair"]}
devicePrefMap={devPrefs}
prefOptions={[
<MenuItem
key={pond.GateDeviceType.GATE_DEVICE_TYPE_UNKNOWN}
value={pond.GateDeviceType.GATE_DEVICE_TYPE_UNKNOWN}>
None
</MenuItem>,
<MenuItem
key={pond.GateDeviceType.GATE_DEVICE_TYPE_PCA}
value={pond.GateDeviceType.GATE_DEVICE_TYPE_PCA}>
PCA Unit
</MenuItem>
]}
devicePrefChanged={(device, pref) => {
updateGateDevicePrefs(device.id(), pref);
}}
open={openDeviceDrawer}
close={() => {
setOpenDeviceDrawer(false);
}}
linkedDevices={devices}
linkedComponents={components}
updateLinkedDevices={(device, linked) => {
let devMap = devices;
let id = device.device?.settings?.deviceId;
if (id) {
if (linked) {
gateAPI
.updateLink(gateID, "gate", id.toString(), "device", [
"read",
"write",
"grant",
"revoke"
])
.then(resp => {
if (id) {
devMap.set(id.toString(), device);
setTabVal(id);
}
setDevices(devMap);
})
.catch(err => {
console.log("error linking device");
});
} else {
gateAPI.updateLink(gateID, "gate", id.toString(), "device", []).then(resp => {
if (id) {
devMap.delete(id.toString());
if (tabVal === id) {
let firstEntry = Array.from(devMap.values())[0];
if (firstEntry) {
setTabVal(firstEntry.device?.settings?.deviceId ?? -1);
} else {
setTabVal(0);
}
}
}
setDevices(devMap);
});
}
}
}}
updateLinkedComponents={(deviceID, component, linked) => {
let c = components;
if (linked) {
gateAPI
.updateLink(gateID, "gate", deviceID + ":" + component.key(), "component", [
"read",
"write",
"grant",
"revoke"
])
.then(resp => {
c.push(component);
setComponents([...c]);
});
} else {
gateAPI
.updateLink(gateID, "gate", deviceID + ":" + component.key(), "component", [])
.then(resp => {
c.forEach((comp, i) => {
if (component.key() === comp.key()) {
c.splice(i, 1);
setComponents([...c]);
}
});
});
}
}}
/>
);
};
const gateDisplay = () => {
return (
<React.Fragment>
{loadingGate ? (
<Card>
<Box display="flex" justifyContent="center" alignItems="center" style={{ height: 300 }}>
<CircularProgress size={200} thickness={1.5} />
</Box>
</Card>
) : (
<React.Fragment>
<Card
style={{
//padding: isMobile ? 2 : 15,
marginLeft: isMobile ? 0 : 15,
marginRight: isMobile ? 0 : 15
}}>
<Tabs
style={{
marginBottom: 5,
display: Array.from(devices.keys()).length < 2 ? "none" : "block"
}}
value={tabVal}
indicatorColor="secondary"
//textColor="secondary"
onChange={(_, newVal) => {
setTabVal(newVal);
}}
aria-label="device tabs">
<Tab label={"Device Not Found"} value={-1} style={{ display: "none" }} />
<Tab label={"No Connected Devices"} value={0} style={{ display: "none" }} />
{Array.from(devices.values()).map(dev => {
let name = "Device Not Found";
let devKey = -1;
if (dev.device && dev.device.settings) {
name = dev.device.settings.name;
devKey = dev.device.settings.deviceId;
}
return <Tab key={devKey} label={name ?? devKey} value={devKey} />;
})}
</Tabs>
{/* panel to show if there are no devices */}
<TabPanelMine value={tabVal} index={0}>
<ButtonBase
onClick={() => {
setOpenDeviceDrawer(true);
}}
style={{ height: 150, width: "100%", margin: -15 }}>
<Grid
container
direction="row"
spacing={2}
alignItems="center"
alignContent="center">
<Grid>
<Box display="flex" justifyContent="center" alignItems="center">
<Link style={{ height: 50, width: 50 }} />
</Box>
</Grid>
<Grid>
<Typography style={{ fontSize: 25, fontWeight: 650 }}>
Connect Device
</Typography>
</Grid>
<Grid size={12}>
Click here to add a device to the gate. To add an additional device to a gate
use the "Link Device" icon on the top right side of this page
</Grid>
</Grid>
</ButtonBase>
</TabPanelMine>
{/* panel to show if there was an issue in the tab */}
<TabPanelMine value={tabVal} index={-1}>
Device Not Found
</TabPanelMine>
{Array.from(devices.values()).map(dev => {
let devKey = 0;
if (dev.device && dev.device.settings) {
devKey = dev.device.settings.deviceId;
if (tabVal === 0) {
setTabVal(devKey);
}
}
return (
<TabPanelMine key={devKey} value={tabVal} index={devKey}>
<GateDevice
key={devKey}
comprehensiveDevice={dev}
gate={gate}
linkedCompList={components}
drawerView={props.useMobile}
/>
</TabPanelMine>
);
})}
</Card>
</React.Fragment>
)}
</React.Fragment>
);
};
const noteDrawer = () => {
return (
<Drawer
open={openNoteDrawer}
onClose={() => {
setOpenNoteDrawer(false);
}}
anchor="right"
classes={{ paper: classes.drawerPaper }}>
<Box padding={2}>
<Typography>Notes</Typography>
<Card style={{ padding: 10 }}>
<Chat objectKey={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} />
</Card>
</Box>
</Drawer>
);
};
return (
<PageContainer>
<ObjectControls
objectButton={
<IconButton
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
onClick={() => {
setDisplayTab(0);
}}
className={displayTab === 0 ? classes.activeButton : classes.inactiveButton}
component="span">
<PlaneIcon />
</IconButton>
}
linkDeviceFunction={() => {
setOpenDeviceDrawer(true);
}}
notesButton={
<IconButton
onClick={() => {
if (props.useMobile || isMobile) {
setDisplayTab(1);
} else {
setOpenNoteDrawer(true);
}
}}
size="small"
style={{
marginTop: "0.3625rem",
marginBottom: "0.3625rem",
marginRight: "0.5rem"
}}
className={displayTab === 1 ? classes.activeButton : classes.inactiveButton}
component="span">
<NotesIcon />
</IconButton>
}
mapFunction={() => {
goToMap();
}}
actions={<GateActions gate={gate} refreshCallback={loadGate} permissions={permissions} />}
permissions={permissions}
devices={Array.from(devices.values()).map(compDev => Device.any(compDev.device))}
/>
<Box marginLeft={2} marginTop={isMobile ? -1.5 : -1} marginBottom={isMobile ? 0 : 1}>
<Typography style={{ fontSize: 25, fontWeight: 650 }}>{gate.name}</Typography>
</Box>
<TabPanelMine value={displayTab} index={0}>
{gateDisplay()}
</TabPanelMine>
{/* tab for notes on mobile and the map drawer */}
<TabPanelMine value={displayTab} index={1}>
<Card style={{ padding: 10 }}>
<Chat objectKey={gate.key} type={pond.NoteType.NOTE_TYPE_GATE} />
</Card>
</TabPanelMine>
{/* drawer is for displaying notes on desktop */}
{noteDrawer()}
{deviceDrawer()}
</PageContainer>
);
}

509
src/pages/Terminals.tsx Normal file
View file

@ -0,0 +1,509 @@
import {
Tab,
Tabs,
Button,
Theme,
useTheme,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
DialogTitle,
DialogContent,
DialogActions
} from "@mui/material";
import { Terminal } from "models";
import { useTerminalAPI, useGlobalState } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import GateSettings from "gate/GateSettings";
import PageContainer from "./PageContainer";
import AddIcon from "@mui/icons-material/Add";
import TerminalSettings from "terminal/TerminalSettings";
import { Delete, MoreVert, ExitToApp as RemoveSelfIcon } from "@mui/icons-material";
import EditIcon from "@mui/icons-material/Edit";
import { blue, green, red } from "@mui/material/colors";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useSnackbar, useUserAPI } from "hooks";
import { Gate } from "models/Gate";
import GateList from "gate/GateList";
import { Scope } from "models";
import { pond } from "protobuf-ts/pond";
import ObjectUsers from "user/ObjectUsers";
import ObjectTeams from "teams/ObjectTeams";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import ShareObject from "user/ShareObject";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import AddGateFab from "gate/AddGateFab";
import { makeStyles } from "@mui/styles";
const parentTab = {
"&": {
margin: "0px",
marginTop: "4px",
marginLeft: "4px",
animationDuration: "10s",
background: "rgba(150, 150, 150, 0)",
borderRadius: "-5px",
borderTopLeftRadius: "6px",
borderTopRightRadius: "6px"
},
"&:hover": {
background: "linear-gradient(rgba(150, 150, 150, 0.2), rgba(150, 150, 150, 0))"
}
};
const useStyles = makeStyles((theme: Theme) => ({
tab: {
...parentTab,
minWidth: theme.spacing(25),
left: 0,
height: "100%",
background: theme.palette.background.paper
},
smallTab: {
...parentTab,
width: theme.spacing(8),
minWidth: theme.spacing(8),
background: theme.palette.background.paper
},
selectedTab: {
borderRadius: "-5px",
borderTopLeftRadius: "6px",
borderTopRightRadius: "6px",
background: theme.palette.background.default,
"&:hover": {
background:
"linear-gradient(rgba(150, 150, 150, 0.3)," + theme.palette.background.default + " 80%)"
}
},
tabText: {
position: "absolute",
left: theme.spacing(2),
color: "transparent",
textAlign: "left",
width: "80%",
backgroundImage: "linear-gradient(to right, white 70%, rgba(200, 200, 200, 0) 87.5%)",
backgroundClip: "text",
WebkitBackgroundClip: "text",
overflowX: "hidden",
whiteSpace: "nowrap",
top: 12
},
icon: {
display: "flex",
position: "absolute",
right: 0,
top: 6,
padding: 6,
marginRight: "2px",
width: 36,
height: 36,
borderRadius: "18px",
background: "rgba(0,0,0,0)",
"&:hover": {
background:
"radial-gradient(closest-side, rgba(150, 150, 150, 0.5) 50%, rgba(150, 150, 150, 0.5))"
}
},
green: {
marginRight: 0,
paddingRight: 0,
color: green["500"],
"&:hover": {
color: green["600"]
}
},
red: {
marginRight: 0,
paddingRight: 0,
color: red["500"],
"&:hover": {
color: red["600"]
}
},
blueIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
addGate: {
position: "absolute",
bottom: 20,
right: 20,
backgroundColor: theme.palette.primary.main
}
}));
interface Props {
terminal?: Terminal;
}
export default function Terminals(props: Props) {
const [loading, setLoading] = useState(false);
const terminalAPI = useTerminalAPI();
const [value, setValue] = useState(0);
const [terminals, setTerminals] = useState<Terminal[]>([]);
const [gates, setGates] = useState<Gate[]>([]);
// map using the terminal key and the gates for that terminal
const [gateMap, setGateMap] = useState<Map<string, Gate[]>>(new Map<string, Gate[]>());
const [displayGates, setDisplayGates] = useState<Gate[]>([]);
const [gateDialog, setGateDialog] = useState(false);
const classes = useStyles();
const theme = useTheme();
const [addTerminal, setAddTerminal] = useState(false);
const [tabClick, setTabClick] = useState(true);
const [menuAnchorEl, setMenuAnchorEl] = useState<Element | null>(null);
const [currentTerminal, setCurrentTerminal] = useState<Terminal>();
const [removeTerminal, setRemoveTerminal] = useState(false);
const { openSnack } = useSnackbar();
const [{ user, as }] = useGlobalState();
const userAPI = useUserAPI();
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [userSharing, setUserSharing] = useState(false);
const [teamSharing, setTeamSharing] = useState(false);
const [baseShareDialog, setBaseShareDialog] = useState(false);
const [removeSelfDialog, setRemoveSelfDialog] = useState(false);
useEffect(() => {
if (currentTerminal) {
let key = currentTerminal?.key;
let kind = "terminal";
if (as) {
key = as;
kind = "team";
}
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
setPermissions(resp.permissions);
});
}
}, [as, userAPI, user, currentTerminal]);
useEffect(() => {
if (props.terminal) {
setCurrentTerminal(props.terminal);
setDisplayGates(gateMap.get(props.terminal.key) ?? []);
}
}, [props.terminal, gateMap]);
const load = useCallback(() => {
console.log(loading)
if (loading) return;
console.log("terminal load")
setLoading(true);
terminalAPI
.listTerminalsAndGates(200, 0)
.then(resp => {
console.log(resp)
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
let allGates = resp.data.gates.map(t => Gate.any(t));
setGates(allGates);
let tMap: Map<string, Gate[]> = new Map<string, Gate[]>();
allGates.forEach(gate => {
let mapEntry = tMap.get(gate.terminal());
if (mapEntry) {
mapEntry.push(gate);
} else {
tMap.set(gate.terminal(), [gate]);
}
setGateMap(tMap);
setDisplayGates(allGates);
});
})
.catch(err => {
openSnack("There was a problem loading your terminals");
})
.finally(() => {
setLoading(false);
});
}, [terminalAPI, openSnack]); //eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
console.log("load use")
load();
}, [load]);
const remove = () => {
if (currentTerminal) {
terminalAPI
.removeTerminal(currentTerminal.key)
.then(resp => {
openSnack("Terminal Deleted");
let a = terminals;
a.splice(terminals.indexOf(currentTerminal), 1);
setTerminals([...a]);
})
.catch(err => {
openSnack("Failed to delete terminal");
})
.finally(() => {
setRemoveTerminal(false);
});
}
};
const terminalMenu = () => {
if (currentTerminal === undefined) return;
return (
<Menu
id="terminalMenu"
anchorEl={menuAnchorEl ? menuAnchorEl : null}
open={menuAnchorEl !== null}
onClose={() => {
setMenuAnchorEl(null);
}}>
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
onClick={() => {
setAddTerminal(true);
setMenuAnchorEl(null);
}}
aria-label="Edit Terminal"
dense>
<ListItemIcon>
<EditIcon className={classes.green} />
</ListItemIcon>
<ListItemText secondary="Edit Terminal" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<MenuItem
onClick={() => {
setRemoveTerminal(true);
setMenuAnchorEl(null);
}}
aria-label="Remove Terminal"
dense>
<ListItemIcon>
<Delete className={classes.red} />
</ListItemIcon>
<ListItemText secondary="Delete Terminal" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<MenuItem
dense
onClick={() => {
setUserSharing(true);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
<MenuItem
dense
onClick={() => {
setTeamSharing(true);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setRemoveSelfDialog(true);
setMenuAnchorEl(null);
}}>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const changeTabs = (newValue: number) => {
if (tabClick && newValue <= terminals.length) {
setValue(newValue);
if (terminals[newValue - 1]) {
setDisplayGates(gateMap.get(terminals[newValue - 1].key) ?? []);
} else {
setDisplayGates(gates);
}
}
};
const gateDisplay = () => {
return (
<GateList
gates={displayGates}
terminals={terminals}
useMobile={props.terminal !== undefined}
/>
);
};
//could update the tabs in the same way as the bin yard tabs
const terminalTabs = () => {
return (
<React.Fragment>
<Tabs
variant="scrollable"
scrollButtons="auto"
value={value}
onChange={(_, v) => changeTabs(v)}
TabIndicatorProps={{ style: { background: "rgba(0,0,0,0)" } }}>
<Tab
key={"all"}
label="All"
disableTouchRipple
classes={{ root: classes.smallTab, selected: classes.selectedTab }}
style={{ marginLeft: theme.spacing(1) }}
/>
{terminals.map((a, index) => (
<Tab
classes={{ root: classes.tab, selected: classes.selectedTab }}
disableTouchRipple
key={a.key}
label={
<span>
<div className={classes.tabText}>{a.name}</div>
<MoreVert
onMouseEnter={() => setTabClick(false)}
onMouseLeave={() => setTabClick(true)}
className={classes.icon}
onClick={event => {
let target = event.currentTarget;
setMenuAnchorEl(target);
setCurrentTerminal(terminals[index]);
}}
/>
</span>
}
/>
))}
<Tab
classes={{ root: classes.smallTab, selected: classes.selectedTab }}
label={terminals.length === 0 ? "Add Terminal" : <AddIcon />}
onClick={() => {
setCurrentTerminal(undefined);
setAddTerminal(true);
}}
disableTouchRipple={true}
/>
</Tabs>
</React.Fragment>
);
};
const removeDialog = () => {
return (
<ResponsiveDialog
open={removeTerminal}
onClose={() => {
setRemoveTerminal(false);
}}>
<DialogTitle>Delete Terminal</DialogTitle>
<DialogContent>Are you sure you wish to delete this Terminal?</DialogContent>
<DialogActions>
<Button>Cancel</Button>
<Button onClick={remove}>Confirm</Button>
</DialogActions>
</ResponsiveDialog>
);
};
const sharingDialogs = () => {
if (!currentTerminal) return;
return (
<React.Fragment>
<ShareObject
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
permissions={permissions}
isDialogOpen={baseShareDialog}
closeDialogCallback={() => {
setBaseShareDialog(false);
}}
/>
<ObjectUsers
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
permissions={permissions}
isDialogOpen={userSharing}
closeDialogCallback={() => setUserSharing(false)}
refreshCallback={() => {}}
/>
<ObjectTeams
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
permissions={permissions}
isDialogOpen={teamSharing}
closeDialogCallback={() => setTeamSharing(false)}
refreshCallback={() => {
return true;
}}
/>
<RemoveSelfFromObject
scope={{ kind: "terminal", key: currentTerminal.key } as Scope}
label={currentTerminal.name}
isDialogOpen={removeSelfDialog}
closeDialogCallback={removed => {
if (removed) {
let a = terminals;
a.splice(terminals.indexOf(currentTerminal), 1);
setTerminals([...a]);
}
setRemoveSelfDialog(false);
}}
/>
</React.Fragment>
);
};
return (
<PageContainer>
<GateSettings
open={gateDialog}
close={newGate => {
if (newGate) {
if (newGate.terminal() !== "") {
let tMap = gateMap;
tMap.get(newGate.terminal())?.push(newGate);
}
let t = gates;
t.push(newGate);
setGates([...t]);
}
setGateDialog(false);
}}
terminals={terminals}
/>
<TerminalSettings
open={addTerminal}
closeDialog={newTerminal => {
if (newTerminal) {
let a = terminals;
a.push(newTerminal);
setTerminals([...a]);
}
setAddTerminal(false);
}}
terminal={currentTerminal}
/>
{!props.terminal && terminalTabs()}
{terminalMenu()}
{removeDialog()}
{sharingDialogs()}
{gateDisplay()}
<AddGateFab
onClick={() => {
setGateDialog(true);
}}
pulse={gates.length < 1}
/>
</PageContainer>
);
}

View file

@ -36,7 +36,7 @@ export {
// useDataDogProxyAPI,
useMineAPI,
// useKeyManagerAPI,
// useTerminalAPI,
useTerminalAPI, //TODO: update api with resolve,reject
useGateAPI,
// useObjectHeaterAPI,
// useMutationAPI,

View file

@ -1,5 +1,5 @@
import { useHTTP } from "hooks";
import React, { createContext, PropsWithChildren, useContext } from "react";
import { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
@ -55,6 +55,18 @@ export interface IGateInterface {
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListGateAirflowResponse>>;
listGateFlowEvents: (
gate: string,
device: number | string,
ambientComponent: string,
pressureComponent: string,
start: string,
end: string,
idleFlow: number,
flowVariance: number,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListGateFlowEventsResponse>>;
listGateMeasurements: (
key: string,
start: string,
@ -274,6 +286,43 @@ export default function GateProvider(props: PropsWithChildren<Props>) {
})
};
const listGateFlowEvents = (
gate: string,
device: number | string,
ambientComponent: string,
pressureComponent: string,
start: string,
end: string,
idleFlow: number,
flowVariance: number,
keys?: string[],
types?: string[]
) => {
return get<pond.ListGateFlowEventsResponse>(
pondURL(
"/gates/" +
gate +
"/airflowEvents?device=" +
device +
"&ambient=" +
ambientComponent +
"&pressure=" +
pressureComponent +
"&start=" +
start +
"&end=" +
end +
"&idle=" +
idleFlow +
"&variance=" +
flowVariance +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
);
};
return (
<GateAPIcontext.Provider
value={{
@ -286,7 +335,8 @@ export default function GateProvider(props: PropsWithChildren<Props>) {
getGatePageData,
updatePrefs,
listGateAirflow,
listGateMeasurements
listGateMeasurements,
listGateFlowEvents
}}>
{children}
</GateAPIcontext.Provider>

View file

@ -24,6 +24,7 @@ import MineProvider, { useMineAPI } from "./mineAPI";
import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI";
import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI";
import TerminalProvider, {useTerminalAPI} from "./terminalAPI"
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -64,7 +65,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<FieldMarkerProvider>
<HomeMarkerProvider>
<HarvestPlanProvider>
<TerminalProvider>
{children}
</TerminalProvider>
</HarvestPlanProvider>
</HomeMarkerProvider>
</FieldMarkerProvider>
@ -115,5 +118,6 @@ export {
useMineAPI,
useFieldMarkerAPI,
useHomeMarkerAPI,
useHarvestPlanAPI
useHarvestPlanAPI,
useTerminalAPI
};

View file

@ -0,0 +1,155 @@
import { useHTTP } from "hooks";
import { createContext, PropsWithChildren, useContext } from "react";
import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond";
import { AxiosResponse } from "axios";
import { useGlobalState } from "providers";
export interface ITerminalAPIContext {
addTerminal: (
name: string,
terminal: pond.TerminalSettings
) => Promise<AxiosResponse<pond.AddTerminalResponse>>;
listTerminals: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string
) => Promise<AxiosResponse<pond.ListTerminalsResponse>>;
listTerminalsAndGates: (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string
) => Promise<AxiosResponse<pond.ListTerminalsAndGatesResponse>>;
updateTerminal: (
key: string,
name: string,
settings: pond.TerminalSettings
) => Promise<AxiosResponse<pond.UpdateTerminalResponse>>;
removeTerminal: (key: string) => Promise<AxiosResponse<pond.RemoveTerminalResponse>>;
}
export const TerminalAPIContext = createContext<ITerminalAPIContext>({} as ITerminalAPIContext);
interface Props {}
export default function TerminalProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get, del, post, put } = useHTTP();
const [{ as }] = useGlobalState();
const addTerminal = (name: string, terminal: pond.TerminalSettings) => {
if (as)
return post<pond.AddTerminalResponse>(
pondURL("/terminals?name=" + name + "&as=" + as),
terminal
);
return post<pond.AddTerminalResponse>(pondURL("/terminals?name=" + name), terminal);
};
const listTerminals = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string
) => {
if (as)
return get<pond.ListTerminalsResponse>(
pondURL(
"/terminals?as=" +
as +
"&limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "")
)
);
return get<pond.ListTerminalsResponse>(
pondURL(
"/terminals?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "")
)
);
};
const listTerminalsAndGates = (
limit: number,
offset: number,
order?: "asc" | "desc",
orderBy?: string,
search?: string
) => {
if (as)
return get<pond.ListTerminalsAndGatesResponse>(
pondURL(
"/terminalsAndGates?as=" +
as +
"&limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "")
)
);
return get<pond.ListTerminalsAndGatesResponse>(
pondURL(
"/terminalsAndGates?limit=" +
limit +
"&offset=" +
offset +
("&order=" + (order ? order : "asc")) +
("&by=" + (orderBy ? orderBy : "key")) +
(search ? "&search=" + search : "")
)
);
};
const updateTerminal = (key: string, name: string, settings: pond.TerminalSettings) => {
if (as) {
return put<pond.UpdateTerminalResponse>(
pondURL("/terminals/" + key + "?as=" + as + "&name=" + name),
settings
);
}
return put<pond.UpdateTerminalResponse>(
pondURL("/terminals/" + key + "?name=" + name),
settings
);
};
const removeTerminal = (key: string) => {
if (as) {
return del<pond.RemoveTerminalResponse>(pondURL("/terminals/" + key + "?as=" + as));
}
return del<pond.RemoveTerminalResponse>(pondURL("/terminals/" + key));
};
return (
<TerminalAPIContext.Provider
value={{
addTerminal,
listTerminals,
updateTerminal,
removeTerminal,
listTerminalsAndGates
}}>
{children}
</TerminalAPIContext.Provider>
);
}
export const useTerminalAPI = () => useContext(TerminalAPIContext);

View file

@ -0,0 +1,92 @@
import { Button, DialogActions, DialogContent, DialogTitle, TextField } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useEffect, useState } from "react";
import { useTerminalAPI } from "providers";
import { useSnackbar } from "hooks";
import { pond } from "protobuf-ts/pond";
import { Terminal } from "models/Terminal";
interface Props {
open: boolean;
closeDialog: (newTerminal?: Terminal, updateTerminal?: Terminal) => void;
long?: number;
lat?: number;
terminal?: Terminal;
}
export default function TerminalSettings(props: Props) {
const { open, closeDialog, terminal, long, lat } = props;
const terminalAPI = useTerminalAPI();
const [name, setName] = useState("");
const { openSnack } = useSnackbar();
useEffect(() => {
if (terminal) {
setName(terminal.name);
} else {
setName("");
}
}, [terminal]);
const confirmChanges = () => {
if (terminal) {
//change settings here, currently no settings to change as the long/lat are changed from the map
terminal.name = name;
//note: changing the terminal object passed in WILL in fact change it in the parent
terminalAPI
.updateTerminal(terminal.key, terminal.name, terminal.settings)
.then(resp => {
openSnack("Terminal updated");
closeDialog();
})
.catch(err => {
openSnack("There was a problem updating the terminal");
});
} else {
let settings: pond.TerminalSettings = pond.TerminalSettings.create({
longitude: long,
latitude: lat
});
terminalAPI
.addTerminal(name, settings)
.then(resp => {
openSnack("New terminal added");
let newTerminal = Terminal.create(
pond.Terminal.create({ key: resp.data.key, name: name, settings: settings })
);
closeDialog(newTerminal);
})
.catch(err => {
openSnack("There was a problem adding the terminal");
});
}
};
return (
<ResponsiveDialog open={open} onClose={() => closeDialog()} fullWidth>
<DialogTitle>Terminal Settings</DialogTitle>
<DialogContent>
<TextField
fullWidth
id="name"
type="text"
label="Terminal Name"
value={name}
onChange={e => setName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
closeDialog();
}}>
Cancel
</Button>
<Button onClick={confirmChanges} variant="contained" color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
}