512 lines
No EOL
20 KiB
TypeScript
512 lines
No EOL
20 KiB
TypeScript
import { Box, Button, Card, 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 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";
|
|
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
|
|
|
|
export interface DevicePageData {
|
|
device: Device;
|
|
components: Component[];
|
|
interactions: Interaction[];
|
|
permissions: pond.Permission[];
|
|
preferences: pond.DevicePreferences;
|
|
// componentPreferences: Map<string, pond.DeviceComponentPreferences>
|
|
}
|
|
|
|
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()
|
|
// console.log(state)
|
|
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 [scannedAddresses, setScannedAddresses] = useState<pond.ComponentAddressMap | undefined>(undefined)
|
|
const [scanInProgress, setScanInProgress] = useState(false)
|
|
// const [components, setComponents] = useState<Component[]>([]);
|
|
|
|
const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false)
|
|
|
|
// const [devicePageData, setDevicePageData] = useState(pond.GetDevicePageDataResponse.create())
|
|
|
|
const loadDevice = () => {
|
|
if (loading) return
|
|
if (state?.devicePageData) {
|
|
setDevice(Device.create(state.devicePageData.device))
|
|
setComponents(state.devicePageData.components.map((comp: pond.Component) => Component.create(comp)))
|
|
setInteractions(state.devicePageData.interactions.map((inter: pond.Interaction) => Interaction.create(inter)))
|
|
setPreferences(state.devicePageData.preferences)
|
|
setPermissions(state.devicePageData.permissions)
|
|
return
|
|
}
|
|
setLoading(true)
|
|
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes(), as).then(resp => {
|
|
// setDevicePageData(resp.data)
|
|
let device = Device.any(resp.data.device)
|
|
// console.log(resp.data)
|
|
setScanInProgress(resp.data.pendingRequests)
|
|
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));
|
|
}
|
|
|
|
const loadPortScan = () => {
|
|
deviceAPI.listFoundComponents(parseInt(deviceID))
|
|
.then(resp => {
|
|
console.log(resp.data)
|
|
if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
|
|
setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.log(err)
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadDevice()
|
|
loadPortScan()
|
|
}, [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,
|
|
expansionLine: c.settings.expansionLine,
|
|
muxLine: c.settings.muxLine
|
|
});
|
|
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}
|
|
scanInProgress={scanInProgress}
|
|
//permissions={permissions}
|
|
components={Array.from(components.values())}
|
|
refreshCallback={loadDevice}
|
|
/>
|
|
</Grid>
|
|
{/* add grid card here for I2C detected components */}
|
|
{(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
|
|
<Grid size={{ xs: 12 }}>
|
|
<DeviceScannedComponents
|
|
scannedComponents={scannedAddresses}
|
|
device={device}
|
|
availablePositions={availablePositions}
|
|
availableOffsets={availableOffsets}
|
|
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: 4 }} >
|
|
<DeviceWizard
|
|
device={device}
|
|
//permissions={permissions}
|
|
components={Array.from(components.values())}
|
|
refreshCallback={loadDevice}
|
|
/>
|
|
</Grid>
|
|
{(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
|
|
<Grid size={{ xs: 6, sm: 6, lg: 8, xl: 8 }}>
|
|
<DeviceScannedComponents
|
|
scannedComponents={scannedAddresses}
|
|
device={device}
|
|
availablePositions={availablePositions}
|
|
availableOffsets={availableOffsets}
|
|
refreshCallback={loadDevice}/>
|
|
</Grid>
|
|
}
|
|
{diagnosticComponents.length > 0 &&
|
|
<Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}>
|
|
<Grid container direction="row" spacing={2} width={"100%"}>
|
|
{diagnosticComponents.map(comp => (
|
|
<Grid size={12} key={comp.key()}>
|
|
<ComponentDiagnostics
|
|
component={comp}
|
|
device={device}
|
|
refreshCallback={loadDevice}
|
|
/>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</Grid>
|
|
}
|
|
</>
|
|
)}
|
|
</Grid>
|
|
</ListItem>
|
|
</>
|
|
)}
|
|
</List>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageContainer spacing={isMobile ? 0 : 2}>
|
|
<Grid container justifyContent={"space-between"}>
|
|
<Grid>
|
|
<SmartBreadcrumb deviceName={device.name()} />
|
|
</Grid>
|
|
<Grid>
|
|
<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()]}
|
|
usage={getUsage()}
|
|
loading={loading}
|
|
groupID={parseInt(groupID)}
|
|
/>
|
|
{componentCards()}
|
|
</PageContainer>
|
|
);
|
|
} |