frontend/src/device/DeviceViewer.tsx

392 lines
13 KiB
TypeScript

import { Box, Grid2 as Grid, Theme, Typography } from "@mui/material";
import {makeStyles} from "@mui/styles"
//import { Theme } from "@material-ui/core/styles/createMuiTheme";
import Loader from "common/Loader";
import MapCard from "common/MapCard";
import { Coordinate } from "common/MapGL";
import ComponentCard from "component/ComponentCard";
import ComponentSettings from "component/ComponentSettings";
import DeviceOverview from "device/DeviceOverview";
import {
useComponentAPI,
useDeviceAPI,
useInteractionsAPI,
useSnackbar,
useUsageAPI,
useUserAPI
} from "hooks";
import { cloneDeep } from "lodash";
import { Component, Device as DeviceModel, deviceScope, Interaction } from "models";
import moment from "moment";
import { sameComponentID, sortComponents } from "pbHelpers/Component";
import {
FindAvailablePositions,
DeviceAvailabilityMap,
OffsetAvailabilityMap
} from "pbHelpers/DeviceAvailability";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState } from "providers";
import React, { useCallback, useEffect, useState } from "react";
//import { Redirect } from "react-router";
import { or } from "utils/types";
import DeviceActions from "./DeviceActions";
const useStyles = makeStyles((theme: Theme) => ({
title: {
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis"
},
noComponents: {
marginTop: theme.spacing(16)
},
addComponentText: {
...theme.typography.subtitle2,
color: theme.palette.primary.main,
cursor: "pointer"
},
deviceLoading: {
position: "relative",
height: "100%",
width: "100%",
display: "flex",
marginTop: theme.spacing(16),
alignItems: "center",
justifyContent: "center"
},
rateLimit: {
marginTop: theme.spacing(1),
color: theme.palette.text.secondary
}
})
);
interface Props {
device: DeviceModel;
isMobile?: boolean;
}
export default function Device(props: Props) {
const classes = useStyles();
const [{ user, as }] = useGlobalState();
const { error } = useSnackbar();
const deviceID = props.device.id();
const deviceAPI = useDeviceAPI();
const componentAPI = useComponentAPI();
const interactionsAPI = useInteractionsAPI();
const usageAPI = useUsageAPI();
const userAPI = useUserAPI();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [device, setDevice] = useState<DeviceModel>(DeviceModel.create());
const [components, setComponents] = useState<Map<string, Component>>(new Map());
const [interactions, setInteractions] = useState<Interaction[]>([]);
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
const [preferences, setPreferences] = useState<pond.UserPreferences>(
pond.UserPreferences.create()
);
const [invalidDevice, setInvalidDevice] = useState<boolean>(false);
const [isAddComponentDialogOpen, setIsAddComponentDialogOpen] = useState<boolean>(false);
const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
const [latitude, setLatitude] = useState<number>(NaN);
const [longitude, setLongitude] = useState<number>(NaN);
const [cellularUsage, setCellularUsage] = useState<number>(0);
const [cellularStatus, setCellularStatus] = useState<string>("");
const rate = 30;
const load = useCallback(() => {
if (user.id() === "") return;
setIsLoading(true);
//let devicePromise = deviceAPI.get(deviceID);
let userPromise = userAPI.getUser(user.id(), deviceScope(deviceID.toString()));
let componentsPromise = componentAPI.list(
deviceID,
undefined,
[deviceID.toString()],
["device"],
true,
as
);
let interactionsPromise = interactionsAPI.listInteractionsByDevice(deviceID, undefined, as);
let groupPromise: Promise<any> = Promise.resolve(undefined);
Promise.all([userPromise, componentsPromise, interactionsPromise, groupPromise])
.then(([userRes, componentsRes, interactionsRes]) => {
//let rDevice = DeviceModel.any(deviceRes.data);
let rawComponents: Array<any> = or(componentsRes.data.components, []);
let rComponents: Map<string, Component> = new Map();
rawComponents.forEach((rawComponent: any) => {
let component = Component.any(rawComponent);
rComponents.set(component.key(), component);
});
setComponents(rComponents);
setInteractions(interactionsRes);
setPermissions(userRes.permissions);
setPreferences(userRes.preferences);
setDevice(props.device);
setLongitude(props.device.status.longitude ? props.device.status.longitude : NaN);
setLatitude(props.device.status.latitude ? props.device.status.latitude : NaN);
let available = FindAvailablePositions(
Array.from(rComponents.values()),
props.device.settings.product
);
setAvailablePositions(available.GetAvailability());
setAvailableOffsets(available.offsetAvailability);
})
.catch((err: any) => {
setDevice(DeviceModel.create());
setComponents(new Map());
setInteractions([]);
setAvailablePositions(new Map());
setAvailableOffsets(new Map());
setPermissions([]);
setPreferences(pond.UserPreferences.create());
setInvalidDevice(true);
error(err);
})
.finally(() => setIsLoading(false));
usageAPI
.getUsage(deviceID, moment().subtract(1, "days"))
.then((res: any) => {
let usage = res.data;
if (usage) {
let rCellularStatus = "active";
let rCellularUsage = 0;
let sessions: any[] = [];
if (usage.sessions) {
sessions = usage.sessions.filter((session: any) => {
return moment(session.begin).isAfter(moment().subtract(1, "days"));
});
}
sessions.forEach((session: any) => (rCellularUsage += Number(or(session.bytes, 0))));
setCellularStatus(rCellularStatus);
setCellularUsage(rCellularUsage);
}
})
.catch(err => {});
}, [componentAPI, props.device, deviceID, error, interactionsAPI, /*usageAPI*/, userAPI, user, as]);
useEffect(() => {
load();
}, [load]);
const getOrderedComponents = () => {
if (preferences.childDisplayOrder && preferences.childDisplayOrder.length === Array.from(components.values()).length) {
return Array.from(components.values()).sort((a, b: Component) =>
sortComponents(a, b, preferences.childDisplayOrder)
);
} else {
return Array.from(components.values());
}
};
const displayedComponentsBlacklist = [
quack.ComponentType.COMPONENT_TYPE_POWER,
quack.ComponentType.COMPONENT_TYPE_MODEM
];
const displayedComponents = () => {
return [...components.values()].filter(
c => !displayedComponentsBlacklist.includes(c.settings.type)
);
};
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 productCard = () => {
// return <DeviceProductCard device={device} components={components} />;
// };
const componentCards = () => {
let orderedComponents = getOrderedComponents();
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;
}
componentCards.push(
<ComponentCard
device={device}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
component={c}
components={orderedComponents}
interactions={filteredInteractions}
permissions={permissions}
key={i}
showMobile={props.isMobile}
refreshCallback={(updatedComponent?: Component) =>
updatedComponent ? handleComponentChanged(updatedComponent) : load()
}
/>
);
}
if (!hasGPS && !isNaN(latitude) && !isNaN(longitude)) {
let path: Array<Coordinate> = [];
path.push({ latitude: latitude, longitude: longitude, timestamp: "" });
componentCards.push(<MapCard key={"map"} path={path} />);
}
return componentCards;
};
const getUsage = () => {
let usage = undefined;
if (cellularStatus && cellularStatus !== "") {
usage = { status: cellularStatus, bytes: cellularUsage };
}
return usage;
};
const anyComponentsReportingFast = () => {
let fast = false;
components.forEach(c => {
if (
c.settings.measurementPeriodMs > 0 &&
c.settings.reportPeriodMs > 0 &&
c.settings.reportPeriodMs < rate * 1000
) {
fast = true;
}
});
return fast;
};
const componentsTab = () => {
return (
<Box padding={1} marginTop={1}>
{anyComponentsReportingFast() && (
<Typography className={classes.rateLimit} variant="caption">
Live updates are rate limited to {rate}s to ensure a smooth browsing experience
</Typography>
)}
{isLoading ? (
<span className={classes.deviceLoading}>
<Loader />
</span>
) : displayedComponents().length > 0 ? (
<Grid container direction="row" spacing={2}>
{componentCards()}
</Grid>
) : (
<Box marginTop={4}>
<Typography variant="subtitle1" align="center" color="textSecondary">
No components found
</Typography>
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
<React.Fragment>
<Typography variant="body2" align="center" color="textSecondary">
<span
className={classes.addComponentText}
onClick={() => setIsAddComponentDialogOpen(true)}>
{" Add "}
</span>
a component to {device.name()}.
</Typography>
<ComponentSettings
mode="add"
device={device}
isDialogOpen={or(isAddComponentDialogOpen, false)}
closeDialogCallback={() => setIsAddComponentDialogOpen(false)}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={load}
canEdit={permissions.includes(pond.Permission.PERMISSION_WRITE)}
/>
</React.Fragment>
)}
</Box>
)}
</Box>
);
};
const isPaused = () => {
let paused = false;
if (cellularStatus && cellularStatus !== "") {
if (cellularStatus.toLowerCase().includes("pause")) {
paused = true;
}
}
return paused;
};
const toggleNotificationPreference = () => {
let updatedPreferences = cloneDeep(preferences);
updatedPreferences.notify = !preferences.notify;
deviceAPI
.updatePreferences(deviceID, updatedPreferences, undefined, undefined, as)
.then(() => setPreferences(updatedPreferences))
.catch(() => {
error(
"Error occured while " +
(preferences.notify ? "enabling" : "disabling") +
" notifications"
);
});
};
// if (invalidDevice) {
// return <Redirect to="/404" />;
// } else {
return (
<Box padding={1}>
<Grid container direction="row" alignItems="center">
<Grid style={{ order: 5 }}>
<DeviceActions
device={device}
isPaused={isPaused()}
components={getOrderedComponents()}
interactions={interactions}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
permissions={permissions}
refreshCallback={load}
preferences={preferences}
toggleNotificationPreference={toggleNotificationPreference}
isLoading={isLoading}
/>
</Grid>
<Grid style={{ order: 4 }}>
<DeviceOverview
device={device}
components={[...components.values()]}
usage={getUsage()}
loading={isLoading}
/>
</Grid>
</Grid>
{componentsTab()}
</Box>
);
//}
}