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 "./FieldMarker";
|
||||||
export * from "./GrainBag";
|
export * from "./GrainBag";
|
||||||
export * from "./Contract";
|
export * from "./Contract";
|
||||||
|
export * from "./Terminal";
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { getWhitelabel } from "services/whiteLabel";
|
||||||
import Ventilation from "pages/VentEditor";
|
import Ventilation from "pages/VentEditor";
|
||||||
import FieldMap from "pages/FieldMap";
|
import FieldMap from "pages/FieldMap";
|
||||||
import GrainBag from "pages/grainBag";
|
import GrainBag from "pages/grainBag";
|
||||||
|
import Terminals from "pages/Terminals";
|
||||||
|
import Gate from "pages/Gate";
|
||||||
|
|
||||||
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
|
||||||
const DevicePage = lazy(() => import("pages/Device"));
|
const DevicePage = lazy(() => import("pages/Device"));
|
||||||
|
|
@ -46,6 +48,7 @@ export default function Router(props: Props) {
|
||||||
<Route path="groups/*" element={<GroupsRoute/>} />
|
<Route path="groups/*" element={<GroupsRoute/>} />
|
||||||
<Route path="bins/*" element={<BinsRoute/>} />
|
<Route path="bins/*" element={<BinsRoute/>} />
|
||||||
<Route path="mines/*" element={<MinesRoute/>} />
|
<Route path="mines/*" element={<MinesRoute/>} />
|
||||||
|
<Route path="terminals/*" element={<TerminalGatesRoute />} />
|
||||||
</Routes>
|
</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 = () => {
|
const GroupsRoute = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,13 @@ import {
|
||||||
// IsAdCon,
|
// IsAdCon,
|
||||||
// isBXT,
|
// isBXT,
|
||||||
IsMiVent,
|
IsMiVent,
|
||||||
|
IsOmniAir,
|
||||||
// IsOmniAir
|
// IsOmniAir
|
||||||
} from "services/whiteLabel";
|
} from "services/whiteLabel";
|
||||||
import MiningIcon from "products/ventilation/MiningIcon";
|
import MiningIcon from "products/ventilation/MiningIcon";
|
||||||
import { useAuth0 } from "@auth0/auth0-react";
|
import { useAuth0 } from "@auth0/auth0-react";
|
||||||
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
||||||
|
import PlaneIcon from "products/AviationIcons/PlaneIcon";
|
||||||
|
|
||||||
const drawerWidth = 230;
|
const drawerWidth = 230;
|
||||||
|
|
||||||
|
|
@ -140,6 +142,7 @@ export default function SideNavigator(props: Props) {
|
||||||
const authenticatedSideMenu = () => {
|
const authenticatedSideMenu = () => {
|
||||||
const isMiVent = IsMiVent();
|
const isMiVent = IsMiVent();
|
||||||
const isAg = IsAdaptiveAgriculture()
|
const isAg = IsAdaptiveAgriculture()
|
||||||
|
const isMiPCA = IsOmniAir()
|
||||||
return (
|
return (
|
||||||
<List className={classes.list} component="nav">
|
<List className={classes.list} component="nav">
|
||||||
{(isAg || user.hasFeature("admin")) && (
|
{(isAg || user.hasFeature("admin")) && (
|
||||||
|
|
@ -156,6 +159,20 @@ export default function SideNavigator(props: Props) {
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Tooltip>
|
</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">
|
<Tooltip title="Devices" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-dashboard"
|
id="tour-dashboard"
|
||||||
|
|
@ -209,7 +226,7 @@ export default function SideNavigator(props: Props) {
|
||||||
{(isAg || user.hasFeature("admin")) && (
|
{(isAg || user.hasFeature("admin")) && (
|
||||||
<Tooltip title="Visual Farm" placement="right">
|
<Tooltip title="Visual Farm" placement="right">
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
id="tour-dashboard"
|
id="tour-visual-farm"
|
||||||
onClick={() => goTo("/visualFarm")}
|
onClick={() => goTo("/visualFarm")}
|
||||||
classes={getClasses("/visualFarm")}
|
classes={getClasses("/visualFarm")}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ export default function Bin(props: Props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
// const match = useRouteMatch<MatchParams>();
|
// const match = useRouteMatch<MatchParams>();
|
||||||
// const binID = binKey ?? match.params.binID;
|
// const binID = binKey ?? match.params.binID;
|
||||||
const binID = useParams<{ binID: string }>()?.binID ?? "";
|
const binID = binKey ?? useParams<{ binID: string }>()?.binID ?? "";
|
||||||
const binAPI = useBinAPI();
|
const binAPI = useBinAPI();
|
||||||
const [{ user }] = useGlobalState();
|
const [{ user }] = useGlobalState();
|
||||||
const [binLoading, setBinLoading] = useState(false);
|
const [binLoading, setBinLoading] = useState(false);
|
||||||
|
|
@ -269,7 +269,7 @@ export default function Bin(props: Props) {
|
||||||
loadRef.current = true;
|
loadRef.current = true;
|
||||||
//add the presets to the bin page data load
|
//add the presets to the bin page data load
|
||||||
binAPI
|
binAPI
|
||||||
.getBinPageData(binKey ?? binID, user.id(), showErrors)
|
.getBinPageData(binID, user.id(), showErrors)
|
||||||
.then(resp => {
|
.then(resp => {
|
||||||
if (resp.data.grainCompositionNames) {
|
if (resp.data.grainCompositionNames) {
|
||||||
let tempMap: Map<string, string> = new Map();
|
let tempMap: Map<string, string> = new Map();
|
||||||
|
|
@ -559,7 +559,7 @@ export default function Bin(props: Props) {
|
||||||
|
|
||||||
const goToDevice = (dev: Device) => {
|
const goToDevice = (dev: Device) => {
|
||||||
if (fromMap) {
|
if (fromMap) {
|
||||||
navigate("/bins/" + binKey + "/devices/" + dev.id(), { replace: true });
|
navigate("/bins/" + binID + "/devices/" + dev.id(), { replace: true });
|
||||||
} else {
|
} else {
|
||||||
navigate(window.location.pathname + "/devices/" + dev.id(), { replace: true });
|
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,
|
// useDataDogProxyAPI,
|
||||||
useMineAPI,
|
useMineAPI,
|
||||||
// useKeyManagerAPI,
|
// useKeyManagerAPI,
|
||||||
// useTerminalAPI,
|
useTerminalAPI, //TODO: update api with resolve,reject
|
||||||
useGateAPI,
|
useGateAPI,
|
||||||
// useObjectHeaterAPI,
|
// useObjectHeaterAPI,
|
||||||
// useMutationAPI,
|
// useMutationAPI,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useHTTP } from "hooks";
|
import { useHTTP } from "hooks";
|
||||||
import React, { createContext, PropsWithChildren, useContext } from "react";
|
import { createContext, PropsWithChildren, useContext } from "react";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { pondURL } from "./pond";
|
import { pondURL } from "./pond";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
|
@ -55,6 +55,18 @@ export interface IGateInterface {
|
||||||
keys?: string[],
|
keys?: string[],
|
||||||
types?: string[]
|
types?: string[]
|
||||||
) => Promise<AxiosResponse<pond.ListGateAirflowResponse>>;
|
) => 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: (
|
listGateMeasurements: (
|
||||||
key: string,
|
key: string,
|
||||||
start: 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 (
|
return (
|
||||||
<GateAPIcontext.Provider
|
<GateAPIcontext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
@ -286,7 +335,8 @@ export default function GateProvider(props: PropsWithChildren<Props>) {
|
||||||
getGatePageData,
|
getGatePageData,
|
||||||
updatePrefs,
|
updatePrefs,
|
||||||
listGateAirflow,
|
listGateAirflow,
|
||||||
listGateMeasurements
|
listGateMeasurements,
|
||||||
|
listGateFlowEvents
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</GateAPIcontext.Provider>
|
</GateAPIcontext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import MineProvider, { useMineAPI } from "./mineAPI";
|
||||||
import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI";
|
import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI";
|
||||||
import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
|
import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI";
|
||||||
import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI";
|
import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI";
|
||||||
|
import TerminalProvider, {useTerminalAPI} from "./terminalAPI"
|
||||||
// import NoteProvider from "providers/noteAPI";
|
// import NoteProvider from "providers/noteAPI";
|
||||||
|
|
||||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||||
|
|
@ -64,7 +65,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
||||||
<FieldMarkerProvider>
|
<FieldMarkerProvider>
|
||||||
<HomeMarkerProvider>
|
<HomeMarkerProvider>
|
||||||
<HarvestPlanProvider>
|
<HarvestPlanProvider>
|
||||||
{children}
|
<TerminalProvider>
|
||||||
|
{children}
|
||||||
|
</TerminalProvider>
|
||||||
</HarvestPlanProvider>
|
</HarvestPlanProvider>
|
||||||
</HomeMarkerProvider>
|
</HomeMarkerProvider>
|
||||||
</FieldMarkerProvider>
|
</FieldMarkerProvider>
|
||||||
|
|
@ -115,5 +118,6 @@ export {
|
||||||
useMineAPI,
|
useMineAPI,
|
||||||
useFieldMarkerAPI,
|
useFieldMarkerAPI,
|
||||||
useHomeMarkerAPI,
|
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