frontend/src/pages/Device.tsx

461 lines
No EOL
17 KiB
TypeScript

import { Button, Divider, List, ListItem, Typography } from "@mui/material";
import Grid from '@mui/material/Grid2';
import { Component, Device, Interaction, User } from "models";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
import { useEffect, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useMobile } from "hooks";
import LoadingScreen from "app/LoadingScreen";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import DeviceActions from "device/DeviceActions";
import { pond, quack } from "protobuf-ts/pond";
import { cloneDeep } from "lodash";
import DeviceOverview from "device/DeviceOverview";
import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import AddComponentManualDialog from "component/AddComponentManualDialog";
import ComponentCard from "component/ComponentCard";
import { sameComponentID, sortComponents } from "pbHelpers/Component";
import { isController } from "pbHelpers/ComponentType";
import { or } from "utils";
import ComponentDiagnostics from "component/ComponentDiagnostics";
import DeviceWizard from "device/DeviceWizard";
export default function DevicePage() {
const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar()
const isMobile = useMobile()
const deviceID = useParams<{ deviceID: string }>()?.deviceID ?? "";
const groupID = useParams<{ groupID: string }>()?.groupID ?? "";
const { state } = useLocation();
const [{ as, team, user }] = useGlobalState()
const [device, setDevice] = useState<Device>(state?.device ? Device.create(state.device) : Device.create())
const [loading, setLoading] = useState(false)
const [permissions, setPermissions] = useState<pond.Permission[]>([])
const [preferences, setPreferences] = useState<pond.DevicePreferences>(pond.DevicePreferences.create())
// const [tags, setTags] = useState<pond.Tag[]>([]);
const [interactions, setInteractions] = useState<Interaction[]>([]);
const [prefsMap, setPrefsMap] = useState<Map<string, pond.DeviceComponentPreferences>>(new Map());
const [cellularUsage, _setCellularUsage] = useState<number>(0);
const [cellularStatus, _setCellularStatus] = useState<string>("");
const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
const [displayOrder, setDisplayOrder] = useState<string[]>([]);
const [components, setComponents] = useState<Map<string, Component>>(new Map());
const [diagnosticComponents, setDiagnosticComponents] = useState<Component[]>([]);
// const [components, setComponents] = useState<Component[]>([]);
const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false)
const loadDevice = () => {
// console.log("load device page data")
if (loading) return
setLoading(true)
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes(), as).then(resp => {
let device = Device.any(resp.data.device)
// console.log(resp.data.device)
setDevice(device)
let newPermissions: pond.Permission[] = []
resp.data.permissions.forEach(perm => {
if (typeof(perm) === "string" ) {
const permNumber = pond.Permission[perm as keyof typeof pond.Permission]
newPermissions.push(permNumber)
} else {
newPermissions.push(perm)
}
})
setPermissions(newPermissions)
let u = User.any(resp.data.user);
setPreferences(u.preferences)
// setTags(resp.data.tags)
resp.data.device?.status?.tagNames
let newComps: Component[] = []
resp.data.components.forEach(comp => {
newComps.push(Component.create(comp))
})
let available = FindAvailablePositions(
newComps,
device.settings.product
);
setAvailablePositions(available.GetAvailability());
let rawComponents: Array<any> = or(resp.data.components, []);
let rComponents: Map<string, Component> = new Map();
let diagComponents: Component[] = [];
rawComponents.forEach((rawComponent: any) => {
let component = Component.any(rawComponent);
rComponents.set(component.key(), component);
//check if is a diagnostic component
switch (component.type()) {
case quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE:
if (component.subType() === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_DIAG) {
diagComponents.push(component);
}
}
});
setDiagnosticComponents(diagComponents)
setComponents(rComponents)
let interactions: Interaction[] = [];
resp.data.interactions.forEach((interaction: pond.Interaction) => {
if (
interaction.settings &&
interaction.settings.nodeOne > interaction.settings.nodeTwo &&
interaction.settings.nodeTwo !== 0
) {
//flip operator and send negative comparitor to save
interaction.settings.conditions.forEach(condition => {
//coming back from the backend as a string for some reason
if (condition.comparison.toString() === "RELATIONAL_OPERATOR_GREATER_THAN") {
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
} else {
condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
}
condition.value = condition.value * -1;
});
}
interactions.push(Interaction.any(interaction));
});
setInteractions(interactions);
}).catch(err => {
setDevice(Device.create());
// setComponents(new Map());
setInteractions([]);
setAvailablePositions(new Map());
setPermissions([]);
setPreferences(pond.UserPreferences.create());
// setInvalidDevice(true);
// error(err);
if (err?.response?.data?.error.includes("not found")) {
let name = as === team.key() ? team.name() : user.name();
let warningString = name + " not permitted to view Device " + deviceID;
let length = getContextTypes().length;
if (length > 0) {
warningString = warningString + " through " + getContextTypes()[length - 1];
}
snackbar.warning(warningString);
} else {
snackbar.error(err);
}
})
.finally(() => setLoading(false));
}
useEffect(() => {
loadDevice()
}, [deviceID, as])
const toggleNotificationPreference = () => {
let updatedPreferences = cloneDeep(preferences);
updatedPreferences.notify = !preferences.notify;
deviceAPI
.updatePreferences(deviceID, updatedPreferences, getContextKeys(), getContextTypes(), as)
.then(() => setPreferences(updatedPreferences))
.catch(() => {
snackbar.error(
"Error occured while " +
(preferences.notify ? "enabling" : "disabling") +
" notifications"
);
});
};
if (loading) return (
<LoadingScreen message="Loading device"/>
)
const getUsage = () => {
let usage = undefined;
if (cellularStatus && cellularStatus !== "") {
usage = { status: cellularStatus, bytes: cellularUsage };
}
return usage;
};
const displayedComponentsBlacklist = [
quack.ComponentType.COMPONENT_TYPE_POWER,
quack.ComponentType.COMPONENT_TYPE_MODEM
];
const getOrderedComponents = () => {
let compsToOrder: Component[] = [];
Array.from(components.values()).forEach(comp => {
if (!displayedComponentsBlacklist.includes(comp.settings.type)) {
compsToOrder.push(comp);
}
});
if (displayOrder.length === compsToOrder.length) {
return compsToOrder.sort((a, b: Component) => sortComponents(a, b, displayOrder));
} else {
return compsToOrder;
}
};
const handleComponentChanged = (component: Component) => {
let updatedComponents = cloneDeep(components);
let updatedComponent = cloneDeep(component);
if (updatedComponents.has(updatedComponent.key())) {
updatedComponents.set(updatedComponent.key(), updatedComponent);
setComponents(updatedComponents);
}
};
const componentCards = () => {
let orderedComponents = getOrderedComponents();
let sensorComponents: JSX.Element[] = [];
let controllerComponents: JSX.Element[] = [];
//let componentCards = [];
//let hasGPS = false;
for (let i = 0; i < orderedComponents.length; i++) {
let c = orderedComponents[i];
if (displayedComponentsBlacklist.includes(c.settings.type)) {
continue;
}
let id: quack.IComponentID = quack.ComponentID.fromObject({
type: c.settings.type,
addressType: c.settings.addressType,
address: c.settings.address
});
let filteredInteractions = interactions.filter(interaction => {
let isSource = false;
let isSink = false;
if (interaction.settings) {
isSource = sameComponentID(interaction.settings.source, id);
isSink = sameComponentID(interaction.settings.sink, id);
}
return isSource || isSink;
});
// if (id.type === quack.ComponentType.COMPONENT_TYPE_GPS) {
// hasGPS = true;
// }
if (isController(c.settings.type)) {
controllerComponents.push(
<ComponentCard
device={device}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
component={c}
components={orderedComponents}
interactions={filteredInteractions}
permissions={permissions}
deviceComponentPreferences={prefsMap.get(c.key())}
key={i}
refreshCallback={(updatedComponent?: Component) =>
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
}
/>
);
} else {
sensorComponents.push(
<ComponentCard
device={device}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
component={c}
components={orderedComponents}
interactions={filteredInteractions}
permissions={permissions}
deviceComponentPreferences={prefsMap.get(c.key())}
key={i}
refreshCallback={(updatedComponent?: Component) =>
updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
}
/>
);
}
}
return (
<List>
<li>
<Grid container direction="row" justifyContent="space-between">
<Grid >
<Typography display="block" variant="h6">
Sensors
</Typography>
</Grid>
<Grid >
{/* <FormControlLabel
label="Show Errors"
control={
<Checkbox
checked={showErrors}
onChange={() => {
//setShowErrors(!showErrors);
dispatch({ key: "showErrors", value: !showErrors });
}}
/>
}
/> */}
</Grid>
</Grid>
</li>
<Divider component="li" />
<ListItem>
<Grid width={"100%"} container justifyContent="flex-start" direction="row" spacing={2}>
{sensorComponents.map((card, index) => (
<Grid key={"sensor-components-"+index} size={{
xs: 12,
sm: isMobile ? 12 : 6,
md: isMobile ? 12 : 6,
lg: isMobile ? 12 : 4,
xl: isMobile ? 12 : 3
}}>
{card}
</Grid>
))}
</Grid>
</ListItem>
<li>
<Typography display="block" variant="h6">
Controls
</Typography>
</li>
<Divider component="li" />
<ListItem>
<Grid width={"100%"} container justifyContent="flex-start" direction="row" spacing={2}>
{controllerComponents.map((card, index) => (
<Grid key={"sensor-components-"+index} size={{
xs: 12,
sm: isMobile ? 12 : 6,
md: isMobile ? 12 : 6,
lg: isMobile ? 12 : 4,
xl: isMobile ? 12 : 3
}}>
{card}
</Grid>
))}
</Grid>
</ListItem>
{device.settings.mutations.length > 0 && (
<>
<li>
<Typography display="block" variant="h6">
Calculated Measurements
</Typography>
</li>
<Divider component="li" />
<ListItem>
<Grid container direction="row" justifyContent="flex-start" spacing={2}>
{/* {device.settings.mutations.map((mut, i) => (
<DeviceMutationCard
key={"mutation" + i}
deviceId={device.id()}
mutation={mut}
showMobile={isMobile}
/>
))} */}
</Grid>
</ListItem>
</>
)}
{device.settings.product !== pond.DeviceProduct.DEVICE_PRODUCT_NONE &&
permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<>
<li>
<Typography display="block" variant="h6">
Setup
</Typography>
</li>
<Divider component="li" />
<ListItem>
<Grid container direction="row" spacing={2} width={"100%"}>
{isMobile ? (
<>
<Grid size={{ xs: 12 }}>
<DeviceWizard
device={device}
//permissions={permissions}
components={Array.from(components.values())}
refreshCallback={loadDevice}
/>
</Grid>
{diagnosticComponents.map(comp => (
<Grid size={{ xs: 12 }} key={comp.key()}>
<ComponentDiagnostics
component={comp}
device={device}
refreshCallback={loadDevice}
/>
</Grid>
))}
</>
) : (
<>
<Grid size={{ xs: 6, sm: 6, lg: 4, xl: 3 }} >
<DeviceWizard
device={device}
//permissions={permissions}
components={Array.from(components.values())}
refreshCallback={loadDevice}
/>
</Grid>
<Grid size={{ xs: 6, sm: 6, lg: 8, xl: 9 }} >
<Grid container direction="row" spacing={2}>
{diagnosticComponents.map(comp => (
<Grid size={{ sm: 12, lg: 6, xl: 4 }} key={comp.key()}>
<ComponentDiagnostics
component={comp}
device={device}
refreshCallback={loadDevice}
/>
</Grid>
))}
</Grid>
</Grid>
</>
)}
</Grid>
</ListItem>
</>
)}
</List>
);
}
return (
<PageContainer padding={isMobile ? 0 : 2}>
<Grid container justifyContent={"space-between"}>
<Grid>
<SmartBreadcrumb />
</Grid>
<Grid>
<AddComponentManualDialog
device={device.id()}
open={addComponentManualDialogOpen}
onClose={() => setAddComponentManualDialogOpen(false)}
/>
{user.hasFeature("developer") === true &&
<Button onClick={() => setAddComponentManualDialogOpen(true)}>
Manual Comp
</Button>
}
<DeviceActions
device={device}
isPaused={false}
isLoading={loading}
permissions={permissions}
preferences={preferences}
toggleNotificationPreference={toggleNotificationPreference}
refreshCallback={loadDevice}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
components={[...components.values()]}
interactions={interactions}
/>
</Grid>
</Grid>
<DeviceOverview
device={device}
components={[...components.values()]}
// components={[]}
// components={components}
usage={getUsage()}
loading={loading}
groupID={parseInt(groupID)}
/>
{componentCards()}
</PageContainer>
);
}