Merge branch 'master' into i2c_detect
This commit is contained in:
commit
33eb4e6eca
47 changed files with 1819 additions and 459 deletions
|
|
@ -208,6 +208,118 @@ export default function Devices() {
|
|||
})
|
||||
}, [tab])
|
||||
|
||||
function compareTimestamps(timestamp1: string, timestamp2: string): number {
|
||||
const date1 = new Date(timestamp1);
|
||||
const date2 = new Date(timestamp2);
|
||||
|
||||
// Check if dates are valid
|
||||
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
|
||||
throw new Error("Invalid RFC3339 timestamp format");
|
||||
}
|
||||
|
||||
// Compare using getTime() for millisecond precision
|
||||
return date1.getTime() - date2.getTime();
|
||||
}
|
||||
|
||||
const doesDeviceHavePlenum = (device: pond.Device) => {
|
||||
if (device.status?.plenum?.temperature && device.status?.plenum?.timestamp) {
|
||||
if (device.status.plenum?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.plenum?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveSen5x = (device: pond.Device) => {
|
||||
if (device.status?.sen5x?.temperature && device.status?.sen5x?.timestamp) {
|
||||
if (device.status.sen5x?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// if (device.status?.o2) setHasO2(true)
|
||||
// if (device.status?.co) setHasCo(true)
|
||||
// if (device.status?.co2) setHasCo2(true)
|
||||
// if (device.status?.no2) setHasNo2(true)
|
||||
|
||||
const doesDeviceHaveO2 = (device: pond.Device) => {
|
||||
if (device.status?.o2?.ppm && device.status?.o2?.timestamp) {
|
||||
if (device.status.sen5x?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.o2?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveCo = (device: pond.Device) => {
|
||||
if (device.status?.co?.ppm && device.status?.co?.timestamp) {
|
||||
if (device.status.co?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.co?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveCo2 = (device: pond.Device) => {
|
||||
if (device.status?.co2?.ppm && device.status?.co2?.timestamp) {
|
||||
if (device.status.co2?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.co2?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const doesDeviceHaveNo2 = (device: pond.Device) => {
|
||||
if (device.status?.no2?.ppm && device.status?.no2?.timestamp) {
|
||||
if (device.status.no2?.timestamp) {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
if (compareTimestamps(device.status.no2?.timestamp, oneDayAgo.toISOString()) < 0) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(hasPlenums)
|
||||
}, [hasPlenums])
|
||||
|
||||
const openProvisionDialog = () => {
|
||||
setIsProvisionDialogOpen(true);
|
||||
};
|
||||
|
|
@ -319,12 +431,13 @@ export default function Devices() {
|
|||
).then(resp => {
|
||||
let newDevices: Device[] = []
|
||||
resp.data.devices.forEach(device => {
|
||||
if (device.status?.plenum?.temperature) setHasPlenums(true)
|
||||
if (device.status?.sen5x?.temperature) setHasSen5x(true)
|
||||
if (device.status?.o2) setHasO2(true)
|
||||
if (device.status?.co) setHasCo(true)
|
||||
if (device.status?.co2) setHasCo2(true)
|
||||
if (device.status?.no2) setHasNo2(true)
|
||||
setHasPlenums(doesDeviceHavePlenum(device))
|
||||
// setHasPlenums(true)
|
||||
setHasSen5x(doesDeviceHaveSen5x(device))
|
||||
setHasO2(doesDeviceHaveO2(device))
|
||||
setHasCo(doesDeviceHaveCo(device))
|
||||
setHasCo2(doesDeviceHaveCo2(device))
|
||||
setHasNo2(doesDeviceHaveNo2(device))
|
||||
newDevices.push(Device.create(device))
|
||||
})
|
||||
setDevices(newDevices)
|
||||
|
|
@ -809,12 +922,12 @@ export default function Devices() {
|
|||
).then(resp => {
|
||||
let newDevices: Device[] = []
|
||||
resp.data.devices.forEach(device => {
|
||||
if (device.status?.plenum?.temperature) setHasPlenums(true)
|
||||
if (device.status?.sen5x?.temperature) setHasSen5x(true)
|
||||
if (device.status?.o2) setHasO2(true)
|
||||
if (device.status?.co) setHasCo(true)
|
||||
if (device.status?.co2) setHasCo2(true)
|
||||
if (device.status?.no2) setHasNo2(true)
|
||||
setHasPlenums(doesDeviceHavePlenum(device))
|
||||
setHasSen5x(doesDeviceHaveSen5x(device))
|
||||
setHasO2(doesDeviceHaveO2(device))
|
||||
setHasCo(doesDeviceHaveCo(device))
|
||||
setHasCo2(doesDeviceHaveCo2(device))
|
||||
setHasNo2(doesDeviceHaveNo2(device))
|
||||
newDevices.push(Device.create(device))
|
||||
})
|
||||
setDevices(currentRows.concat(newDevices))
|
||||
|
|
|
|||
314
src/pages/Field.tsx
Normal file
314
src/pages/Field.tsx
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
import { Field, fieldScope, HarvestPlan, teamScope } from "models";
|
||||
import { useFieldAPI, useGlobalState, useHarvestPlanAPI } from "providers";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import PageContainer from "./PageContainer";
|
||||
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
||||
import { Box, Card, Grid2, IconButton, Tab, Tabs, Theme, Typography, darken } from "@mui/material";
|
||||
import FieldActions from "field/FieldActions";
|
||||
import { Settings } from "@mui/icons-material";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import FieldMinimap from "field/Fieldminimap";
|
||||
import Weather from "weather/weather";
|
||||
import TaskViewer from "tasks/TaskViewer";
|
||||
import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay";
|
||||
import { cloneDeep } from "lodash";
|
||||
import HarvestPlanTable from "harvestPlan/HarvestPlanTable";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import FieldSettings from "field/FieldSettings";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
card: {
|
||||
height: "100%",
|
||||
bgcolor: darken(theme.palette.background.paper, 0.05),
|
||||
},
|
||||
sectionHeader: {
|
||||
fontSize: 35,
|
||||
fontWeight: 650
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
style={{ height: "94%", paddingTop: "15px" }}
|
||||
{...other}>
|
||||
{value === index && <React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FieldPage() {
|
||||
const { state } = useLocation();
|
||||
//const [{ as }] = useGlobalState()
|
||||
const classes = useStyles();
|
||||
const fieldAPI = useFieldAPI();
|
||||
const userAPI = useUserAPI();
|
||||
const [{as, user}] = useGlobalState();
|
||||
const {openSnack} = useSnackbar();
|
||||
const fieldID = useParams<{ fieldID: string }>()?.fieldID ?? "";
|
||||
const [field, setField] = useState<Field>(state?.field ? Field.create(state.field) : Field.create())
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>(state?.permissions ? state.permissions : [])
|
||||
const isMobile = useMobile()
|
||||
const [openFieldSettings, setOpenFieldSettings] = useState(false)
|
||||
const [planLoading, setPlanLoading] = useState(false)
|
||||
const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
|
||||
const [loading, setLoading] = useState(false)
|
||||
const hPlanAPI = useHarvestPlanAPI();
|
||||
const [tabVal, setTabVal] = useState(0)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const loadField = useCallback(() => {
|
||||
if(state.field){
|
||||
fieldAPI.getField(state.field).then(resp => {
|
||||
setField(Field.create(resp.data))
|
||||
}).catch(err => {
|
||||
|
||||
})
|
||||
}
|
||||
},[as])
|
||||
|
||||
const goTo = () => {
|
||||
navigate("/fields");
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
// if already loading do nothing
|
||||
if(loading) return
|
||||
//if the field is passed through in the state just use it
|
||||
if(state?.field){
|
||||
let field = Field.create(state.field)
|
||||
field.permissions = state.permissions
|
||||
setField(field)
|
||||
setPermissions(state.permissions)
|
||||
return
|
||||
}
|
||||
//otherwise load the field
|
||||
setLoading(true)
|
||||
fieldAPI.getField(fieldID, as).then(resp => {
|
||||
setField(Field.create(resp.data))
|
||||
|
||||
}).catch(err => {
|
||||
openSnack("Failed to load Field")
|
||||
}).finally(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
//also get the permissions
|
||||
let scope = fieldScope(fieldID);
|
||||
if (as) {
|
||||
scope = teamScope(as)
|
||||
}
|
||||
userAPI.getUser(user.id(), scope).then(resp => {
|
||||
let f = cloneDeep(field)
|
||||
f.permissions = resp.permissions
|
||||
setField(f)
|
||||
setPermissions(resp.permissions);
|
||||
});
|
||||
},[loadField, fieldID, as])
|
||||
|
||||
useEffect(() => {
|
||||
if (field.key() !== "") {
|
||||
hPlanAPI
|
||||
.listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
|
||||
.then(resp => {
|
||||
if (resp.data.harvestPlan.length > 0) {
|
||||
let plan = resp.data.harvestPlan[0];
|
||||
setHPlan(HarvestPlan.any(plan));
|
||||
} else {
|
||||
setHPlan(HarvestPlan.create());
|
||||
}
|
||||
setPlanLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
//openSnack("Failed to load plan");
|
||||
});
|
||||
}
|
||||
}, [field, as, hPlanAPI]);
|
||||
|
||||
const fieldActions = () => {
|
||||
return (
|
||||
<Grid2 container direction={"row"} justifyContent="space-between" alignContent="center" alignItems="center">
|
||||
<Grid2>
|
||||
<Typography className={classes.sectionHeader}>
|
||||
{field.name()}
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setOpenFieldSettings(true)
|
||||
}}><Settings /></IconButton>
|
||||
<FieldActions field={field} permissions={permissions} refreshCallback={()=>{}}/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)
|
||||
}
|
||||
|
||||
const map = () => {
|
||||
return (
|
||||
<Box minHeight={550} height="100%">
|
||||
<FieldMinimap field={field} borderSpacing={0.001} squared/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const weather = () => {
|
||||
return (<Weather longitude={field.center().longitude} latitude={field.center().latitude} />)
|
||||
}
|
||||
|
||||
const harvestPlans = () => {
|
||||
return (
|
||||
<HarvestPlanDisplay
|
||||
plan={hPlan}
|
||||
permissions={permissions}
|
||||
planField={field}
|
||||
loading={planLoading}
|
||||
changePlan={updatedPlan => {
|
||||
if (updatedPlan) {
|
||||
setHPlan(updatedPlan);
|
||||
}
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
|
||||
const tasks = () => {
|
||||
let taskLoadKeys: string[] = [];
|
||||
if (!planLoading) {
|
||||
field.key() !== "" && taskLoadKeys.push(field.key());
|
||||
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
|
||||
}
|
||||
return (<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton={isMobile}/>)
|
||||
}
|
||||
|
||||
const desktopView = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box>
|
||||
<Grid2 container spacing={1}>
|
||||
<Grid2 size={4}>
|
||||
<Card raised className={classes.card}>
|
||||
{map()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2 size={3}>
|
||||
<Card raised className={classes.card}>
|
||||
{weather()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2 size={5}>
|
||||
<Card raised className={classes.card}>
|
||||
{tasks()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography className={classes.sectionHeader}>
|
||||
Harvest Overview
|
||||
</Typography>
|
||||
<Grid2 container spacing={1}>
|
||||
<Grid2 size={4}>
|
||||
<Card raised className={classes.card}>
|
||||
{harvestPlans()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<Card raised>
|
||||
<HarvestPlanTable field={field}/>
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const mobileView = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tabs
|
||||
value={tabVal}
|
||||
onChange={(_, val) => {
|
||||
setTabVal(val)
|
||||
}}
|
||||
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
|
||||
<Tab label="Overview" />
|
||||
<Tab label="Activities" />
|
||||
<Tab label="Harvest" />
|
||||
</Tabs>
|
||||
<TabPanel value={tabVal} index={0}>
|
||||
<Grid2 container spacing={2} direction="column">
|
||||
<Grid2 size={12}>
|
||||
<Card raised sx={{height: "50vh"}}>
|
||||
{map()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2 size={12}>
|
||||
<Card raised>
|
||||
{weather()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={1}>
|
||||
<Card raised>
|
||||
{tasks()}
|
||||
</Card>
|
||||
</TabPanel>
|
||||
<TabPanel value={tabVal} index={2}>
|
||||
<Grid2>
|
||||
<Grid2 size={12}>
|
||||
<Card raised className={classes.card}>
|
||||
{harvestPlans()}
|
||||
</Card>
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
<HarvestPlanTable field={field}/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</TabPanel>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const fieldSettings = () => {
|
||||
return (
|
||||
<FieldSettings
|
||||
open={openFieldSettings}
|
||||
onClose={() => {
|
||||
setOpenFieldSettings(false)
|
||||
}}
|
||||
selectedField={field}
|
||||
updateFields={() => {
|
||||
loadField()
|
||||
}}
|
||||
removeField={()=>{
|
||||
goTo()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Box padding={2}>
|
||||
{fieldActions()}
|
||||
{isMobile ? mobileView() : desktopView()}
|
||||
</Box>
|
||||
{fieldSettings()}
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -39,8 +39,9 @@ export default function Heaters() {
|
|||
objectHeaterAPI
|
||||
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
|
||||
.then(resp => {
|
||||
// setHeaters(resp.data.heaterData ?? []);
|
||||
setTotal(resp.data.total)
|
||||
setHeaters(resp.data.heaterData);
|
||||
setHeaters(resp.data.heaterData ?? []);
|
||||
});
|
||||
}, [objectHeaterAPI, as, pageSize, tablePage, searchText]);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export default function Tasks() {
|
|||
return (
|
||||
<PageContainer>
|
||||
<Box padding={2}>
|
||||
<TaskViewer />
|
||||
<TaskViewer overlayButton/>
|
||||
</Box>
|
||||
</PageContainer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -182,7 +182,9 @@ export default function Terminals(props: Props) {
|
|||
if(loading) return
|
||||
setLoading(true)
|
||||
terminalAPI.listTerminals(200, 0, "asc", "name", as).then(resp => {
|
||||
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
||||
if(resp.data.terminals){
|
||||
setTerminals(resp.data.terminals.map(a => Terminal.any(a)));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log("There was a problem loading Terminals")
|
||||
}).finally(() => {
|
||||
|
|
|
|||
|
|
@ -108,7 +108,8 @@ export default function Users() {
|
|||
"remove-devices",
|
||||
"copy-token",
|
||||
"recluse",
|
||||
"pause-data"
|
||||
"pause-data",
|
||||
"access-object"
|
||||
].sort();
|
||||
const features = [
|
||||
"admin",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue