imported the gate and terminal related files to prep for the aviation map
This commit is contained in:
parent
e2e061151b
commit
505cb8e3aa
25 changed files with 4868 additions and 9 deletions
131
src/charts/SingleSetAreaChart.tsx
Normal file
131
src/charts/SingleSetAreaChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
323
src/common/DeviceLinkDrawer.tsx
Normal file
323
src/common/DeviceLinkDrawer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
105
src/ducting/DuctDescriber.tsx
Normal file
105
src/ducting/DuctDescriber.tsx
Normal 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
57
src/gate/AddGateFab.tsx
Normal 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
203
src/gate/GateActions.tsx
Normal 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
413
src/gate/GateDevice.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
255
src/gate/GateDeviceInteraction.tsx
Normal file
255
src/gate/GateDeviceInteraction.tsx
Normal 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
437
src/gate/GateFlowGraph.tsx
Normal 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
357
src/gate/GateGraphs.tsx
Normal 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
143
src/gate/GateList.tsx
Normal 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
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
574
src/gate/GateSettings.tsx
Normal 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
99
src/models/Gate.ts
Normal 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
59
src/models/Terminal.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,3 +24,4 @@ export * from "./Note";
|
|||
export * from "./FieldMarker";
|
||||
export * from "./GrainBag";
|
||||
export * from "./Contract";
|
||||
export * from "./Terminal";
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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")}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
476
src/pages/Gate.tsx
Normal 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
509
src/pages/Terminals.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ export {
|
|||
// useDataDogProxyAPI,
|
||||
useMineAPI,
|
||||
// useKeyManagerAPI,
|
||||
// useTerminalAPI,
|
||||
useTerminalAPI, //TODO: update api with resolve,reject
|
||||
useGateAPI,
|
||||
// useObjectHeaterAPI,
|
||||
// useMutationAPI,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{children}
|
||||
<TerminalProvider>
|
||||
{children}
|
||||
</TerminalProvider>
|
||||
</HarvestPlanProvider>
|
||||
</HomeMarkerProvider>
|
||||
</FieldMarkerProvider>
|
||||
|
|
@ -115,5 +118,6 @@ export {
|
|||
useMineAPI,
|
||||
useFieldMarkerAPI,
|
||||
useHomeMarkerAPI,
|
||||
useHarvestPlanAPI
|
||||
useHarvestPlanAPI,
|
||||
useTerminalAPI
|
||||
};
|
||||
|
|
|
|||
155
src/providers/pond/terminalAPI.tsx
Normal file
155
src/providers/pond/terminalAPI.tsx
Normal 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);
|
||||
92
src/terminal/TerminalSettings.tsx
Normal file
92
src/terminal/TerminalSettings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue