393 lines
13 KiB
TypeScript
393 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" justifyContent="space-between" alignItems="center" width="100%">
|
|
<Grid size={12}>
|
|
<DeviceOverview
|
|
device={device}
|
|
components={[...components.values()]}
|
|
usage={getUsage()}
|
|
loading={isLoading}
|
|
/>
|
|
</Grid>
|
|
<Grid size={12}>
|
|
<DeviceActions
|
|
device={device}
|
|
isPaused={isPaused()}
|
|
components={[...components.values()]}
|
|
//components={getOrderedComponents()}
|
|
interactions={interactions}
|
|
availablePositions={availablePositions}
|
|
availableOffsets={availableOffsets}
|
|
permissions={permissions}
|
|
refreshCallback={load}
|
|
preferences={preferences}
|
|
toggleNotificationPreference={toggleNotificationPreference}
|
|
isLoading={isLoading}
|
|
/>
|
|
</Grid>
|
|
{componentsTab()}
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
//}
|
|
}
|