Merge branch '3d_bin' into staging_environment
This commit is contained in:
commit
370134a401
58 changed files with 10125 additions and 75 deletions
|
|
@ -21,6 +21,10 @@ import {
|
|||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Typography,
|
||||
TextField,
|
||||
RadioGroup,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
} from "@mui/material";
|
||||
import BinActions from "bin/BinActions";
|
||||
import BinHistory from "bin/BinHistory";
|
||||
|
|
|
|||
563
src/pages/BinV2.tsx
Normal file
563
src/pages/BinV2.tsx
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import PageContainer from "./PageContainer";
|
||||
import { DevicePreset } from "models/DevicePreset";
|
||||
import { Controller } from "models/Controller";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMobile, useSnackbar } from "hooks";
|
||||
import { Component, Device, Bin as IBin, Interaction } from "models";
|
||||
import { useBinAPI, useGlobalState } from "providers";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { Plenum } from "models/Plenum";
|
||||
import { Ambient } from "models/Ambient";
|
||||
import { GrainCable } from "models/GrainCable";
|
||||
import { Pressure } from "models/Pressure";
|
||||
import { CO2 } from "models/CO2";
|
||||
import moment from "moment";
|
||||
import { quack } from "protobuf-ts/quack";
|
||||
import { Box, CircularProgress, Drawer, Grid2 as Grid, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Tab, Tabs, Theme, Tooltip } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import NotesIcon from "@mui/icons-material/Notes";
|
||||
import TasksIcon from "products/Construction/TasksIcon";
|
||||
import BindaptIcon from "products/Bindapt/BindaptIcon";
|
||||
import FieldsIcon from "products/AgIcons/FieldsIcon";
|
||||
import BinActions from "bin/BinActions";
|
||||
import BinSummary from "bin/binSummary/BinSummary";
|
||||
import { Close } from "@mui/icons-material";
|
||||
import React from "react";
|
||||
import BinHistory from "bin/BinHistory";
|
||||
import Chat from "chat/Chat";
|
||||
import TaskViewer from "tasks/TaskViewer";
|
||||
|
||||
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((theme: Theme) => {
|
||||
const themeType = theme.palette.mode;
|
||||
return ({
|
||||
spacer: {
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
padding: "auto",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
avatar: {
|
||||
color: themeType === "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
|
||||
},
|
||||
menuPaper: {
|
||||
borderRadius: "20px"
|
||||
},
|
||||
drawerPaperDesktop: {
|
||||
height: "100%",
|
||||
width: "30%"
|
||||
},
|
||||
drawerPaperMobile: {
|
||||
height: "50%",
|
||||
width: "100%"
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
export default function BinV2(){
|
||||
const isMobile = useMobile();
|
||||
const binID = useParams<{ binID: string }>()?.binID ?? "";
|
||||
const binAPI = useBinAPI();
|
||||
const classes = useStyles()
|
||||
const navigate = useNavigate()
|
||||
const [{ user, as, showErrors }] = useGlobalState();
|
||||
const [binLoading, setBinLoading] = useState(false);
|
||||
const loadRef = useRef<boolean>(false);
|
||||
const [bin, setBin] = useState<IBin>(IBin.create());
|
||||
const [devices, setDevices] = useState<Device[]>([]);
|
||||
const [components, setComponents] = useState<Map<string, Component>>(new Map());
|
||||
// const [interactions, setInteractions] = useState<Interaction[]>([]);
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||
const [preferences, setPreferences] = useState<Map<string, pond.BinComponentPreferences>>();
|
||||
const [plenums, setPlenums] = useState<Plenum[]>([]);
|
||||
// const [ambients, setAmbients] = useState<Ambient[]>([]);
|
||||
const [grainCables, setGrainCables] = useState<GrainCable[]>([]);
|
||||
// const [pressures, setPressures] = useState<Pressure[]>([]);
|
||||
// const [headspaceCO2, setHeadspaceCO2] = useState<CO2[]>([]);
|
||||
const [heaters, setHeaters] = useState<Controller[]>([]);
|
||||
const {openSnack} = useSnackbar()
|
||||
const [fans, setFans] = useState<Controller[]>([]);
|
||||
// const [compositionNameMap, setCompositionNameMap] = useState<Map<string, string>>(new Map());
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [noteTab, setNoteTab] = useState(0)
|
||||
|
||||
|
||||
const [componentDevices, setComponentDevices] = useState<Map<string, number>>(
|
||||
new Map<string, number>()
|
||||
);
|
||||
const [interactionDevices, setInteractionDevices] = useState<Map<string, number>>(
|
||||
new Map<string, number>()
|
||||
);
|
||||
const [binPresets, setBinPresets] = useState<DevicePreset[]>([]);
|
||||
const [missedReadings, setMissedReadings] = useState(0);
|
||||
|
||||
//Drawer states
|
||||
const [noteDrawerOpen, setNoteDrawerOpen] = useState(false)
|
||||
const [taskDrawerOpen, setTaskDrawerOpen] = useState(false)
|
||||
|
||||
//DATA LOAD/UPDATE FUNCTIONS
|
||||
|
||||
const load = useCallback(() => {
|
||||
if (loadRef.current || user.id() === "") return;
|
||||
setBinLoading(true);
|
||||
loadRef.current = true;
|
||||
//add the presets to the bin page data load
|
||||
binAPI
|
||||
.getBinPageData(binID, user.id(), showErrors, as)
|
||||
.then(resp => {
|
||||
// if (resp.data.grainCompositionNames) {
|
||||
// let tempMap: Map<string, string> = new Map();
|
||||
// Object.keys(resp.data.grainCompositionNames).forEach(key => {
|
||||
// tempMap.set(key, resp.data.grainCompositionNames[key]);
|
||||
// });
|
||||
// tempMap.set("correction", "Unknown Source");
|
||||
// setCompositionNameMap(tempMap);
|
||||
// }
|
||||
|
||||
let devs: Device[] = [];
|
||||
let p = new Map<string, pond.BinComponentPreferences>();
|
||||
Object.keys(resp.data.preferences).forEach(k => {
|
||||
let prefKey = k.split(":")[1] ? k.split(":")[1] : k;
|
||||
p.set(prefKey, pond.BinComponentPreferences.fromObject(resp.data.preferences[k]));
|
||||
});
|
||||
setPreferences(p);
|
||||
let r: pond.GetBinPageDataResponse = pond.GetBinPageDataResponse.fromObject(resp.data);
|
||||
setBin(IBin.any(resp.data.bin));
|
||||
let newComponentDevices = new Map<string, number>();
|
||||
let attachedDevIds: number[] = [];
|
||||
if (resp.data.componentDevices)
|
||||
Object.keys(resp.data.componentDevices).forEach(k => {
|
||||
newComponentDevices.set(k, resp.data.componentDevices[k]);
|
||||
//make a list of all of the ids for devices that components are currently attached
|
||||
if (!attachedDevIds.includes(resp.data.componentDevices[k])) {
|
||||
attachedDevIds.push(resp.data.componentDevices[k]);
|
||||
}
|
||||
});
|
||||
|
||||
setComponentDevices(newComponentDevices);
|
||||
|
||||
let newInteractionDevices = new Map<string, number>();
|
||||
if (r.interactionDevices)
|
||||
Object.keys(r.interactionDevices).forEach(k => {
|
||||
newInteractionDevices.set(k, r.interactionDevices[k]);
|
||||
});
|
||||
setInteractionDevices(newInteractionDevices);
|
||||
|
||||
// if (resp.data.interactions) {
|
||||
// setInteractions(resp.data.interactions.map((i: pond.Interaction) => Interaction.any(i)));
|
||||
// } else {
|
||||
// setInteractions([]);
|
||||
// }
|
||||
//go through the devices and if the device has an attached component include it in the device list
|
||||
if (resp.data.devices && resp.data.devices[0]) {
|
||||
resp.data.devices.forEach((dev: any) => {
|
||||
let device = Device.create(dev);
|
||||
if (attachedDevIds.includes(device.id())) {
|
||||
devs.push(device);
|
||||
}
|
||||
});
|
||||
setDevices(devs);
|
||||
}
|
||||
if (resp.data.components) {
|
||||
let compMap: Map<string, Component> = new Map();
|
||||
resp.data.components.forEach((comp: pond.Component) => {
|
||||
if (comp && comp.settings) {
|
||||
let c = Component.any(comp);
|
||||
compMap.set(comp.settings.key, c);
|
||||
}
|
||||
});
|
||||
setComponents(compMap);
|
||||
}
|
||||
if (resp.data.presets) {
|
||||
setBinPresets(
|
||||
resp.data.presets.map((preset: pond.DevicePreset) => DevicePreset.create(preset))
|
||||
);
|
||||
}
|
||||
setPermissions(r.permissions ? r.permissions : []);
|
||||
setBinLoading(false);
|
||||
loadRef.current = false;
|
||||
})
|
||||
.finally(() => {
|
||||
setBinLoading(false);
|
||||
loadRef.current = false;
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [binID, user.id(), binAPI, showErrors, as]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
const setBinComponents = useCallback(() => {
|
||||
if (!preferences) return;
|
||||
var unassigned: Component[] = [];
|
||||
var plenums: Plenum[] = [];
|
||||
var ambients: Ambient[] = [];
|
||||
var grainCables: GrainCable[] = [];
|
||||
var fans: Controller[] = [];
|
||||
var heaters: Controller[] = [];
|
||||
var pressures: Pressure[] = [];
|
||||
var headspaceCo2: CO2[] = [];
|
||||
var mostMissed: number = 0;
|
||||
let now = moment();
|
||||
|
||||
components.forEach(comp => {
|
||||
let pref = preferences.get(comp.key());
|
||||
if (pref) {
|
||||
if (pref.type) {
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_PLENUM)
|
||||
plenums.push(Plenum.create(comp));
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_AMBIENT)
|
||||
ambients.push(Ambient.create(comp));
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) {
|
||||
//check cable for missed measurements
|
||||
let cable = GrainCable.create(comp);
|
||||
let lastRead = moment(cable.lastReading);
|
||||
let elapsedMS = now.diff(lastRead);
|
||||
if (elapsedMS > cable.settings.measurementPeriodMs) {
|
||||
let missedReadings = Math.floor(elapsedMS / cable.settings.measurementPeriodMs);
|
||||
if (missedReadings > mostMissed) {
|
||||
mostMissed = missedReadings;
|
||||
}
|
||||
}
|
||||
grainCables.push(cable);
|
||||
}
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEATER)
|
||||
heaters.push(Controller.create(comp));
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_FAN) fans.push(Controller.create(comp));
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_PRESSURE)
|
||||
pressures.push(Pressure.create(comp));
|
||||
if (pref.type === pond.BinComponent.BIN_COMPONENT_HEADSPACE) {
|
||||
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_CO2) {
|
||||
//check if there are missed readings from the co2 and compare them to what we already had
|
||||
let coComp = CO2.create(comp)
|
||||
let lastRead = moment(coComp.lastReading);
|
||||
let elapsedMS = now.diff(lastRead);
|
||||
if (elapsedMS > coComp.settings.measurementPeriodMs) {
|
||||
let missedReadings = Math.floor(elapsedMS / coComp.settings.measurementPeriodMs);
|
||||
if (missedReadings > mostMissed) {
|
||||
mostMissed = missedReadings;
|
||||
}
|
||||
}
|
||||
headspaceCo2.push(coComp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unassigned.push(comp);
|
||||
}
|
||||
}
|
||||
});
|
||||
setMissedReadings(mostMissed);
|
||||
setPlenums(plenums);
|
||||
setGrainCables(grainCables);
|
||||
// setPressures(pressures);
|
||||
// setAmbients(ambients);
|
||||
setHeaters(heaters);
|
||||
setFans(fans);
|
||||
// setHeadspaceCO2(headspaceCo2);
|
||||
}, [components, preferences]);
|
||||
|
||||
useEffect(() => {
|
||||
setBinComponents();
|
||||
}, [setBinComponents]);
|
||||
|
||||
const updateStatus = (componentKeys: string[], removed?: boolean) => {
|
||||
componentKeys.forEach(compKey => {
|
||||
let comp = components.get(compKey);
|
||||
if (comp) {
|
||||
//determine what part of the status to update based on the component
|
||||
//for lidar
|
||||
if (comp.type() === quack.ComponentType.COMPONENT_TYPE_LIDAR) {
|
||||
//determine how to update the status based on if added or removed
|
||||
if (removed) {
|
||||
bin.status.distance = 0;
|
||||
} else {
|
||||
if (
|
||||
comp.lastMeasurement[0] &&
|
||||
comp.lastMeasurement[0].values[0] &&
|
||||
comp.lastMeasurement[0].values[0].values[0]
|
||||
) {
|
||||
bin.status.distance = comp.lastMeasurement[0].values[0].values[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
//update the bins status with the new values based on the components changed
|
||||
binAPI.updateBinStatus(bin.key(), bin.status, as);
|
||||
};
|
||||
|
||||
//NAVIGATION FUNCTIONS
|
||||
const goToDevice = (dev: Device) => {
|
||||
navigate(window.location.pathname + "/devices/" + dev.id(), { replace: true });
|
||||
};
|
||||
|
||||
const goToYard = (bin: IBin) => {
|
||||
navigate("/visualFarm", { state: {
|
||||
long: bin.getLocation()?.longitude,
|
||||
lat: bin.getLocation()?.latitude
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* UI STUFF STARTS HERE
|
||||
*/
|
||||
|
||||
const deviceMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
id="groupMenu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
keepMounted
|
||||
classes={{ paper: classes.menuPaper }}
|
||||
disableAutoFocusItem>
|
||||
{devices.map(dev => (
|
||||
<MenuItem key={dev.id()} onClick={() => goToDevice(dev)}>
|
||||
<ListItemIcon>
|
||||
<BindaptIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={dev.name()} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Header - consists of the actions and things that are always visible like
|
||||
* opening the note/history and tasks drawers, navigating to the device and map pages, opening binsensors and settings etc.
|
||||
* also has the bin name/model and activity and the dropdown for changing sections
|
||||
*/
|
||||
const pageHeader = () => {
|
||||
return (
|
||||
<Box margin={2} marginBottom={0}>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignContent="center"
|
||||
alignItems="center">
|
||||
<Grid >
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setNoteDrawerOpen(true)
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
className={classes.avatar}
|
||||
component="span">
|
||||
<div className={classes.spacer}>
|
||||
<NotesIcon />
|
||||
</div>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setTaskDrawerOpen(true)
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
className={classes.avatar}
|
||||
component="span">
|
||||
<div className={classes.spacer}>
|
||||
<TasksIcon />
|
||||
</div>
|
||||
</IconButton>
|
||||
{devices.length > 1 ? (
|
||||
<Tooltip key="devices" title="devices">
|
||||
<IconButton
|
||||
key="devButton"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
size="small"
|
||||
style={{
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
className={classes.avatar}
|
||||
component="span">
|
||||
<BindaptIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
devices.map(dev => (
|
||||
<Tooltip key={dev.id()} title={dev.name()}>
|
||||
<IconButton
|
||||
key={dev.id()}
|
||||
onClick={() => goToDevice(dev)}
|
||||
size="small"
|
||||
style={{
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
className={classes.avatar}
|
||||
component="span">
|
||||
<div className={classes.spacer}>
|
||||
<BindaptIcon />
|
||||
</div>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
))
|
||||
)}
|
||||
{bin.binMapped() && (
|
||||
<Tooltip key={"goToMap"} title={"Go to Yard"}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
goToYard(bin);
|
||||
}}
|
||||
size="small"
|
||||
style={{
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
className={classes.avatar}
|
||||
component="span">
|
||||
<FieldsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid>
|
||||
<BinActions
|
||||
bin={bin}
|
||||
permissions={permissions}
|
||||
refreshCallback={() => {
|
||||
load();
|
||||
}}
|
||||
userID={user.id()}
|
||||
components={components}
|
||||
setComponents={setComponents}
|
||||
componentDevices={componentDevices}
|
||||
setComponentDevices={setComponentDevices}
|
||||
updateBinStatus={updateStatus}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setNoteTab(newValue);
|
||||
};
|
||||
|
||||
|
||||
//DRAWERS
|
||||
const noteDrawer = () => {
|
||||
return (
|
||||
<Drawer
|
||||
open={noteDrawerOpen}
|
||||
anchor={isMobile ? "bottom" : "right"}
|
||||
classes={{ paper: isMobile ? classes.drawerPaperMobile : classes.drawerPaperDesktop }}
|
||||
onClose={() => setNoteDrawerOpen(false)}>
|
||||
<IconButton style={{ width: 50 }} onClick={() => setNoteDrawerOpen(false)}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
<Tabs
|
||||
value={noteTab}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={handleChange}
|
||||
aria-label="disabled tabs example"
|
||||
// centered={!(isMobile || displayMobile)}
|
||||
style={{
|
||||
marginLeft: "auto"
|
||||
//marginTop: isMobile || displayMobile || drawer ? "-3rem" : "0.25rem"
|
||||
}}>
|
||||
<Tab label="Notes" />
|
||||
<Tab label="History" />
|
||||
</Tabs>
|
||||
<TabPanelMine value={noteTab} index={0}>
|
||||
{/* <Box height={isMobile || displayMobile ? "80vh" : "90vh"} padding={2}> */}
|
||||
<Chat parent={binID} parentType={"bin"} type={pond.NoteType.NOTE_TYPE_BIN} />
|
||||
{/* </Box> */}
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={noteTab} index={1}>
|
||||
<BinHistory drawer binID={binID} />
|
||||
</TabPanelMine>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
const taskDrawer = () => {
|
||||
return (
|
||||
<Drawer
|
||||
style={{ zIndex: 1099 }}
|
||||
open={taskDrawerOpen}
|
||||
anchor={isMobile ? "bottom" : "right"}
|
||||
classes={{ paper: isMobile ? classes.drawerPaperMobile : classes.drawerPaperDesktop }}
|
||||
onClose={() => setTaskDrawerOpen(false)}>
|
||||
<Box>
|
||||
<IconButton style={{ width: 50 }} onClick={() => setTaskDrawerOpen(false)}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
<TaskViewer drawerView keys={[binID]} types={["bin"]} overlayButton />
|
||||
</Box>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{deviceMenu()}
|
||||
{pageHeader()}
|
||||
<BinSummary
|
||||
bin={bin}
|
||||
loading={binLoading}
|
||||
devices={devices}
|
||||
fans={fans}
|
||||
heaters={heaters}
|
||||
cables={grainCables}
|
||||
plenums={plenums}
|
||||
permissions={permissions}
|
||||
componentDevices={componentDevices}
|
||||
componentMap={components}
|
||||
binPrefs={preferences}
|
||||
setPreferences={setPreferences}
|
||||
updateBinCallback={(bin) => {
|
||||
setBin(bin)
|
||||
binAPI.updateBin(bin.key(), bin.settings).then(resp => {
|
||||
openSnack("Bin has Been Updated")
|
||||
})
|
||||
}}
|
||||
setBin={setBin}
|
||||
/>
|
||||
{/* render drawers */}
|
||||
{noteDrawer()}
|
||||
{taskDrawer()}
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue