added tons of files for component functionality; manual component adding added temporarily

This commit is contained in:
Carter 2025-02-11 16:31:03 -06:00
parent 58830d480e
commit 45ff49bcea
121 changed files with 25883 additions and 65 deletions

View file

@ -0,0 +1,98 @@
import { Dialog, DialogActions, DialogContent, DialogTitle, Grid2, TextField } from "@mui/material";
import CancelSubmit from "common/CancelSubmit";
import { useComponentAPI, useSnackbar } from "hooks";
import { pond } from "protobuf-ts/pond";
import { useState } from "react";
import { or } from "utils";
interface Props {
open: boolean;
onClose: () => void;
device: number;
}
export default function AddComponentManualDialog (props: Props) {
const { open, onClose, device } = props;
const componentAPI = useComponentAPI();
const snackbar = useSnackbar();
const [component, setComponent] = useState<pond.ComponentSettings>(pond.ComponentSettings.create())
const onSubmit = () => {
componentAPI.add(device, component).then(() => {
snackbar.success("Component added")
})
}
const close = () => {
setComponent(pond.ComponentSettings.create())
onClose()
}
return (
<Dialog
open={open}
onClose={onClose}
>
<DialogTitle>
Manual Component Entry
</DialogTitle>
<DialogContent>
<Grid2 container direction="column" justifyContent={"center"} spacing={2}>
<Grid2>
<TextField
value={component.name}
fullWidth
label="Name"
onChange={event => {
let newSettings = pond.ComponentSettings.create(component)
newSettings.name = event.currentTarget.value
setComponent(newSettings)
}}
/>
</Grid2>
<Grid2 container direction="row">
<Grid2 size={{ xs: 4 }}>
<TextField
value={component.type}
type="number"
label="Type"
onChange={event => {
let newSettings = pond.ComponentSettings.create(component)
newSettings.type = or(parseInt(event.currentTarget.value), 0)
setComponent(newSettings)
}}
/>
</Grid2>
<Grid2 size={{ xs: 4 }}>
<TextField
value={component.addressType}
type="number"
label="Address Type"
onChange={event => {
let newSettings = pond.ComponentSettings.create(component)
newSettings.addressType = or(parseInt(event.currentTarget.value), 0)
setComponent(newSettings)
}}
/>
</Grid2>
<Grid2 size={{ xs: 4 }}>
<TextField
value={component.address}
type="number"
label="Address"
onChange={event => {
let newSettings = pond.ComponentSettings.create(component)
newSettings.address = or(parseInt(event.currentTarget.value), 0)
setComponent(newSettings)
}}
/>
</Grid2>
</Grid2>
</Grid2>
</DialogContent>
<DialogActions>
<CancelSubmit onCancel={close} onSubmit={onSubmit} />
</DialogActions>
</Dialog>
)
}

View file

@ -0,0 +1,360 @@
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
makeStyles,
Step,
StepLabel,
Stepper,
createStyles,
Grid,
FormControlLabel,
Switch,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Box,
Typography,
Checkbox
} from "@material-ui/core";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Component, Device } from "models";
import { pond, quack } from "protobuf-ts/pond";
import { useComponentAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react";
import { getThemeType } from "theme";
import { CableInfo } from "./ComponentDiagnostics";
import ComponentForm from "./ComponentForm";
const useStyles = makeStyles(() =>
createStyles({
buttons: {
marginLeft: 5,
marginRight: 5
},
cell: {
padding: 5
},
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 0
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 0
}
})
);
interface Props {
open: boolean;
closeDialog: () => void;
port: string;
diagComponent: Component;
device: Device;
cableInfo: CableInfo[];
refreshCallback: () => void;
}
interface CompStep {
label: string;
completed?: boolean;
}
export default function AddCompsFromDiag(props: Props) {
const { diagComponent, port, cableInfo, open, closeDialog, device, refreshCallback } = props;
const [steps, setSteps] = useState<CompStep[]>([]);
const classes = useStyles();
const [currentStep, setCurrentStep] = useState(0);
const [components, setComponents] = useState<Component[]>([]);
const { error, success } = useSnackbar();
//const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false)
const compAPI = useComponentAPI();
const [componentsToAdd, setComponentsToAdd] = useState<Component[]>([]);
const [settingsValid, setSettingsValid] = useState(false);
const [useAdvanced, setUseAdvanced] = useState(false);
useEffect(() => {
let components: Component[] = [];
let steps: CompStep[] = [];
cableInfo.forEach(cable => {
let settings: pond.ComponentSettings = pond.ComponentSettings.create();
settings.type = quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE;
settings.subtype = cable.subtype ?? quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_NONE;
settings.addressType = cable.id + 8;
settings.address = diagComponent.settings.address;
settings.measurementPeriodMs = 3600000; //set the measurement/report period to 1 hour
settings.reportPeriodMs = 3600000;
settings.name = cable.name;
components.push(Component.create(pond.Component.create({ settings: settings })));
steps.push({ label: "Cable ID: " + cable.id });
});
setComponents(components);
setSteps(steps);
}, [cableInfo, diagComponent]);
const stepper = () => {
return (
<Stepper nonLinear activeStep={currentStep} style={{ width: "100%" }}>
{steps.map(compStep => {
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step key={compStep.label} completed={compStep.completed}>
<StepLabel {...labelProps}>{compStep.label}</StepLabel>
</Step>
);
})}
</Stepper>
);
};
const close = () => {
closeDialog();
setUseAdvanced(false);
setComponentsToAdd([]);
};
const addComponents = () => {
let c: pond.MultiComponentSettings = pond.MultiComponentSettings.create();
componentsToAdd.forEach(component => {
c.components.push(component.settings);
});
compAPI
.addMultiComponents(device.id(), c)
.then(resp => {
success("Components added to Device");
})
.catch(err => {
error("One or more component failed to add");
})
.finally(() => {
refreshCallback();
});
};
const resetSteps = () => {
steps.forEach(step => {
step.completed = false;
});
};
const simpleActions = () => {
return (
<DialogActions>
<Grid container direction="row" justify="space-between">
<Grid item>
<Button
variant="contained"
color="primary"
onClick={() => {
close();
}}>
Cancel
</Button>
</Grid>
<Grid item>
<Button
className={classes.buttons}
variant="contained"
color="primary"
onClick={() => {
addComponents();
}}>
Confirm
</Button>
</Grid>
</Grid>
</DialogActions>
);
};
const advancedActions = () => {
return (
<DialogActions>
<Grid container direction="row" justify="space-between">
<Grid item>
<Button
variant="contained"
color="primary"
onClick={() => {
close();
}}>
Cancel
</Button>
</Grid>
<Grid item>
{currentStep !== 0 && (
<Button
className={classes.buttons}
variant="contained"
color="primary"
onClick={() => {
setCurrentStep(currentStep - 1);
}}>
Back
</Button>
)}
{currentStep !== steps.length - 1 && (
<React.Fragment>
<Button
className={classes.buttons}
variant="contained"
color="primary"
onClick={() => {
setCurrentStep(currentStep + 1);
}}>
Next
</Button>
</React.Fragment>
)}
{steps[currentStep] && !steps[currentStep].completed && (
<Button
className={classes.buttons}
variant="contained"
color="primary"
disabled={!settingsValid}
onClick={() => {
//if not on the last step
if (currentStep !== steps.length - 1) {
setCurrentStep(currentStep + 1);
}
steps[currentStep].completed = true;
setSteps([...steps]);
let c = componentsToAdd;
c.push(components[currentStep]);
setComponentsToAdd([...c]);
}}>
Add
</Button>
)}
<Button
className={classes.buttons}
variant="contained"
color="primary"
onClick={() => {
//setConfirmationDialogOpen(true)
addComponents();
}}>
Confirm
</Button>
</Grid>
</Grid>
</DialogActions>
);
};
const simpleContent = () => {
return (
<DialogContent>
<Box>
<Typography>Select the Components to add to the device</Typography>
<Typography>Components will have their measurement interval set to 1 hour</Typography>
</Box>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" className={classes.cell}>
Add Component
</TableCell>
<TableCell align="center" className={classes.cell}>
Cable ID
</TableCell>
<TableCell align="center" className={classes.cell}>
Component Name
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{components.map((comp, i) => (
<TableRow className={i % 2 === 0 ? classes.light : classes.dark} key={i}>
<TableCell align="center" className={classes.cell}>
<Checkbox
onChange={(e, checked) => {
if (checked) {
let c = componentsToAdd;
c.push(comp);
setComponentsToAdd([...c]);
} else {
let c = componentsToAdd;
if (c.includes(comp)) {
c.splice(c.indexOf(comp), 1);
}
}
}}
/>
</TableCell>
<TableCell align="center" className={classes.cell}>
{comp.settings.addressType - 8}
</TableCell>
<TableCell align="center" className={classes.cell}>
{comp.name()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</DialogContent>
);
};
const advancedContent = () => {
return (
<DialogContent>
{stepper()}
<ComponentForm
component={components[currentStep]}
canEdit
device={device}
componentChanged={(component, isValid) => {
components[currentStep].settings = component.settings;
setSettingsValid(isValid);
}}
/>
</DialogContent>
);
};
const componentsDialog = () => {
return (
<ResponsiveDialog
open={open}
onClose={() => {
close();
}}>
<DialogTitle>
<Grid container direction="row" justify="space-between">
<Grid item>Port {port} Components</Grid>
<Grid item>
<FormControlLabel
control={
<Switch
value={useAdvanced}
title="Advanced"
onClick={() => {
setUseAdvanced(!useAdvanced);
setComponentsToAdd([]);
setCurrentStep(0);
resetSteps();
}}
/>
}
label="Advanced"
labelPlacement="start"
/>
</Grid>
</Grid>
</DialogTitle>
{useAdvanced ? advancedContent() : simpleContent()}
{useAdvanced ? advancedActions() : simpleActions()}
</ResponsiveDialog>
);
};
return <React.Fragment>{componentsDialog()}</React.Fragment>;
}

View file

@ -0,0 +1,213 @@
import {
createStyles,
Divider,
IconButton,
ListItemIcon,
ListItemText,
makeStyles,
Menu,
MenuItem,
Tooltip
} from "@material-ui/core";
import { green, teal } from "@material-ui/core/colors";
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import { AddCircle, DeleteOutline, MoreVert, SaveAlt, Settings } from "@material-ui/icons";
import ExportDataSettings from "component/ExportDataSettings";
import InteractionSettings from "interactions/InteractionSettings";
import { Component, Device } from "models";
import { Moment } from "moment";
import { isController, isSource } from "pbHelpers/ComponentType";
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import React, { useState } from "react";
import { or } from "utils/types";
import ComponentSettings from "./ComponentSettings";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
addIcon: {
color: green["500"],
"&:hover": {
color: green["600"]
}
},
copyIcon: {
color: teal["500"],
"&:hover": {
color: teal["600"]
}
},
exportDataIcon: {
color: green["300"],
"&:hover": {
color: green["400"]
}
},
red: {
color: "var(--status-alert)"
}
})
);
interface Props {
device: Device;
component: Component;
components: Component[];
availablePositions: DeviceAvailabilityMap;
availableOffsets: OffsetAvailabilityMap;
permissions: Array<pond.Permission>;
refreshCallback: () => void;
initialStartDate?: Moment;
initialEndDate?: Moment;
deviceComponentPreferences?: pond.DeviceComponentPreferences;
}
export default function ComponentActions(props: Props) {
const classes = useStyles();
const {
device,
component,
components,
availablePositions,
availableOffsets,
permissions,
refreshCallback,
initialStartDate,
initialEndDate,
deviceComponentPreferences
} = props;
const [isComponentSettingsOpen, setIsComponentSettingsOpen] = useState<boolean>(false);
const [isAddInteractionOpen, setIsAddInteractionOpen] = useState<boolean>(false);
const [isExportDataOpen, setIsExportDataOpen] = useState<boolean>(false);
const [anchorEl, setAnchorEl] = useState<Element | null>(null);
const [componentSettingsMode, setComponentSettingsMode] = useState<string>("");
const [isJSON, setIsJSON] = useState(false);
const [{ user, newStructure }] = useGlobalState();
const openMoreMenu = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
setAnchorEl(event.currentTarget);
};
const closeMoreMenu = () => {
setAnchorEl(null);
setIsComponentSettingsOpen(false);
};
const openComponentSettingsDialog = (mode: string) => {
setIsComponentSettingsOpen(true);
setComponentSettingsMode(or(mode, ""));
};
const closeComponentSettingsDialog = () => {
setIsComponentSettingsOpen(false);
};
const menu = () => {
const canEdit = permissions.includes(pond.Permission.PERMISSION_WRITE);
const canAddInteraction =
canEdit && (isSource(component.settings.type) || isController(component.settings.type));
return (
<Menu
id="moreMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMoreMenu}
disableAutoFocusItem>
{canAddInteraction && (
<MenuItem dense onClick={() => setIsAddInteractionOpen(true)} button divider>
<ListItemIcon>
<AddCircle className={classes.addIcon} />
</ListItemIcon>
<ListItemText primary="Add Interaction" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setIsJSON(false);
setIsExportDataOpen(true);
}}>
<ListItemIcon>
<SaveAlt fontSize="default" className={classes.exportDataIcon} />
</ListItemIcon>
<ListItemText primary="Export Data" />
</MenuItem>
{canEdit && <Divider />}
{canEdit && (
<MenuItem dense onClick={() => openComponentSettingsDialog("remove")}>
<ListItemIcon>
<DeleteOutline fontSize="default" className={classes.red} />
</ListItemIcon>
<ListItemText primary="Delete" />
</MenuItem>
)}
</Menu>
);
};
const dialogs = () => {
const canEdit = permissions.includes(pond.Permission.PERMISSION_WRITE);
type Mode = "add" | "remove" | "update" | undefined;
return (
<React.Fragment>
<ComponentSettings
mode={or(componentSettingsMode as Mode, "")}
device={device}
component={component}
isDialogOpen={isComponentSettingsOpen}
closeDialogCallback={closeComponentSettingsDialog}
refreshCallback={refreshCallback}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
canEdit={canEdit}
deviceComponentPrefs={deviceComponentPreferences}
/>
<InteractionSettings
device={device}
components={components}
initialComponent={component}
mode="add"
isDialogOpen={isAddInteractionOpen}
closeDialogCallback={() => setIsAddInteractionOpen(false)}
refreshCallback={refreshCallback}
canEdit={canEdit}
/>
<ExportDataSettings
device={device}
component={component}
isDialogOpen={isExportDataOpen}
closeDialogCallback={() => setIsExportDataOpen(false)}
initialStartDate={initialStartDate}
initialEndDate={initialEndDate}
isJSON={isJSON}
newMeasurements={newStructure}
user={user}
/>
</React.Fragment>
);
};
return (
<React.Fragment>
<Tooltip
title="Settings"
aria-label="Component Settings"
onOpen={event => event.stopPropagation()}>
<IconButton color="default" onClick={() => openComponentSettingsDialog("")}>
<Settings fontSize="default" />
</IconButton>
</Tooltip>
<Tooltip title="More">
<IconButton
aria-owns={Boolean(anchorEl) ? "More" : undefined}
aria-haspopup="true"
onClick={openMoreMenu}>
<MoreVert />
</IconButton>
</Tooltip>
{menu()}
{dialogs()}
</React.Fragment>
);
}

View file

@ -0,0 +1,390 @@
import {
Avatar,
Box,
Card,
CardActionArea,
CardContent,
CardHeader,
Grid2 as Grid,
Switch,
Theme,
Tooltip,
Typography
} from "@mui/material";
import EventBlocker from "common/EventBlocker";
import MeasurementSummary from "component/MeasurementSummary";
import { useComponentAPI, useSnackbar, useThemeType } from "hooks";
import InteractionsOverview from "interactions/InteractionsOverview";
import { cloneDeep } from "lodash";
import { Component, Device, Interaction } from "models";
import { getFriendlyAddressTypeName, getHumanReadableAddress } from "pbHelpers/AddressType";
import { controllerModeLabel } from "pbHelpers/Component";
import {
//extension,
GetComponentIcon,
getMeasurements,
isController
} from "pbHelpers/ComponentType";
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import { findInteractionsAsSource, HasInteraction } from "pbHelpers/Interaction";
import { canWrite } from "pbHelpers/Permission";
import { pond, quack } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
// import { useHistory } from "react-router";
import { hasDeviceFeature } from "services/feature/service";
import ComponentActions from "./ComponentActions";
import { or } from "utils";
import { extractNodes } from "pbHelpers/ComponentTypes";
import UnitMeasurementSummary from "./UnitMeasurementSummary";
import { UnitMeasurement } from "models/UnitMeasurement";
import { useGlobalState } from "providers";
import { makeStyles } from "@mui/styles";
import { useLocation, useNavigate } from "react-router-dom";
const useStyles = makeStyles((theme: Theme) => {
return ({
card: {
position: "relative",
display: "flex",
height: "100%",
flexDirection: "column",
overflow: "visible"
},
cardHeader: {
padding: theme.spacing(1),
paddingLeft: theme.spacing(2)
},
cardContent: {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "center",
padding: theme.spacing(1),
paddingTop: 0
},
moreDetails: {
marginLeft: "auto"
},
avatarIcon: {
width: theme.spacing(3),
height: theme.spacing(3)
},
sensorDot: {
position: "absolute",
marginLeft: theme.spacing(1),
marginTop: theme.spacing(1)
}
})
});
interface Sensor {
label: string;
color: string;
value: string;
}
interface Props {
device: Device;
component: Component;
components: Component[];
availablePositions: DeviceAvailabilityMap;
availableOffsets: OffsetAvailabilityMap;
permissions: pond.Permission[];
interactions: Interaction[];
refreshCallback: (updatedComponent?: Component) => void;
showMobile?: boolean;
showSensors?: boolean;
deviceComponentPreferences?: pond.DeviceComponentPreferences;
}
export default function ComponentCard(props: Props) {
const componentAPI = useComponentAPI();
const classes = useStyles();
const themeType = useThemeType();
const { error, success } = useSnackbar();
const {
device,
component,
components,
availablePositions,
availableOffsets,
permissions,
refreshCallback,
interactions,
showMobile,
showSensors,
deviceComponentPreferences
} = props;
// const history = useHistory();
const navigate = useNavigate()
const location = useLocation()
const [sensors, setSensors] = useState<Sensor[]>([]);
const [{ user, showErrors }] = useGlobalState();
const updateControllerState = (checked: boolean) => {
let updatedComponent = cloneDeep(component);
let newMode =
checked === false ? 2 : HasInteraction(component.location(), interactions) ? 0 : 1;
updatedComponent.settings.defaultOutputState = newMode;
let describe = controllerModeLabel(newMode);
componentAPI
.update(device.id(), updatedComponent.settings)
.then(() => {
success(component.name() + "'s mode was set to " + describe);
refreshCallback(updatedComponent);
})
.catch(() => {
error("Failed to set " + component.name() + "'s mode");
});
};
const pathToComponent = () => {
let url = location.pathname + "/components/" + component.key();
url = url.replace("//", "/");
return url;
};
const openComponentPage = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
const { name, className } = event.target as HTMLButtonElement;
const blacklist = ["slider", "controllerSwitch"];
if (
blacklist.includes(name) ||
blacklist.find(
v =>
className &&
className
.toString()
.toLowerCase()
.includes(v)
) !== undefined
) {
event.preventDefault();
} else {
navigate(pathToComponent());
}
};
const getAddressDescription = () => {
if (
component.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY ||
(component.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&
component.settings.addressType <= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET100)
) {
let addressDescription = getHumanReadableAddress(
component.settings.addressType,
component.settings.type,
component.settings.address,
device.settings.product
);
if (addressDescription === "") {
addressDescription = "Internal";
}
let port = "Port: " + addressDescription;
let cableID = "";
if (
component.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&
component.settings.addressType <= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET100
) {
cableID = "Cable: " + (component.settings.addressType - 8);
}
return port + " " + cableID;
} else {
return getFriendlyAddressTypeName(component.settings.addressType);
}
};
const header = () => {
const canEdit = canWrite(permissions);
const componentIcon = GetComponentIcon(
component.settings.type,
component.settings.subtype,
themeType
);
const name = component.name();
let measurements: UnitMeasurement[] = [];
if (component.status.lastGoodMeasurement)
measurements = component.status.lastGoodMeasurement.map(um => UnitMeasurement.any(um, user));
if (showErrors)
measurements = component.lastMeasurement.map(um => UnitMeasurement.any(um, user));
return (
<CardHeader
avatar={
componentIcon ? (
<Avatar
variant="square"
src={componentIcon}
className={classes.avatarIcon}
alt={name}
/>
) : (
<Box className={classes.avatarIcon} />
)
}
title={
<React.Fragment>
<Tooltip
title={
<div>
Subtype: {component.subTypeName()}
<br />
ID: {component.key()}
<br />
Location: {component.locationString()}
</div>
}
placement="top">
<span>
{name} - <Typography variant="caption">{getAddressDescription()}</Typography>
</span>
</Tooltip>
{isController(component.settings.type) &&
hasDeviceFeature(device.settings.upgradeChannel, "better-controls") && (
<Box component="span" paddingLeft={1}>
<Switch
checked={
component.settings.defaultOutputState === 0 ||
component.settings.defaultOutputState === 1
}
color="default"
size="small"
onTouchStart={event => {
event.stopPropagation();
event.preventDefault();
}}
onMouseDown={event => {
event.stopPropagation();
event.preventDefault();
}}
onClick={event => {
event.stopPropagation();
event.preventDefault();
}}
disabled={!canEdit}
onChange={(_, checked) => updateControllerState(checked)}
name="controllerSwitch"
inputProps={{ "aria-label": "controller switch" }}
/>
</Box>
)}
</React.Fragment>
}
className={classes.cardHeader}
titleTypographyProps={{ variant: "subtitle1" }}
subheader={
// !newStructure ? (
// <MeasurementSummary
// component={component}
// reading={component.status.lastMeasurement}
// dense
// />
// ) : (
<UnitMeasurementSummary
component={component}
excludedNodes={deviceComponentPreferences?.excludedNodes}
reading={UnitMeasurement.convertLastMeasurement(measurements)}
//dense
/>
// )
}
action={
<EventBlocker>
<ComponentActions
device={device}
component={component}
components={components}
availablePositions={availablePositions}
availableOffsets={availableOffsets}
refreshCallback={refreshCallback}
permissions={permissions}
deviceComponentPreferences={deviceComponentPreferences}
/>
</EventBlocker>
}
/>
);
};
const content = () => {
if (findInteractionsAsSource(component.location(), interactions).length <= 0) return null;
return (
<CardContent className={classes.cardContent}>
<EventBlocker>
<InteractionsOverview
device={device}
component={component}
components={components}
interactions={interactions}
permissions={permissions}
refreshCallback={() => refreshCallback()}
/>
</EventBlocker>
</CardContent>
);
};
useEffect(() => {
if (!component) return;
if (!component.status) return;
if (!component.status.lastMeasurement) return;
let measurements = getMeasurements(component.type(), or(component.settings.subtype, 0));
let nodes = extractNodes(component.status!.lastMeasurement!.measurement!);
let sensors: Sensor[] = [];
nodes.forEach(node => {
Object.values(node).forEach((value, index) => {
if (measurements[index]) {
let sensor: Sensor = {
label: measurements[index].label,
value: value,
color: measurements[index].colour
};
sensors.push(sensor);
}
});
});
setSensors(sensors);
}, [component, setSensors]);
const sensorCard = () => {
if (sensors.length <= 0 || !showSensors) return null;
return (
<CardContent className={classes.cardContent}>
<EventBlocker>
{sensors.map((sensor, index) => {
return (
<Typography variant="body2" key={index}>
{sensor.label}: {sensor.value}
</Typography>
);
})}
</EventBlocker>
</CardContent>
);
};
return (
<Grid
size={{
xs: 12,
sm: showMobile ? 12 : 6,
md: showMobile ? 12 : 6,
lg: showMobile ? 12 : 4,
xl: showMobile ? 12 : 3
}}
>
<Card raised className={classes.card}>
<CardActionArea
onClick={(event: any) => openComponentPage(event)}
component="div"
style={{ height: "100%" }}>
{header()}
{content()}
{sensorCard()}
</CardActionArea>
</Card>
</Grid>
);
}

View file

@ -0,0 +1,824 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Card,
CardContent,
Checkbox,
CircularProgress,
Divider,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
Grid2 as Grid,
Theme,
Typography
} from "@mui/material";
import { lightGreen } from "@mui/material/colors";
import { ExpandMore } from "@mui/icons-material";
import MeasurementsChart from "charts/MeasurementsChart";
//import { GraphOrientation } from "common/Graph";
import RangeInput, { Range } from "common/RangeInput";
import { GetDefaultDateRange } from "common/time/DateRange";
import DateSelect from "common/time/DateSelect";
import MeasurementSummary from "component/MeasurementSummary";
import { grainName } from "grain";
import { useMobile, usePrevious } from "hooks";
import { cloneDeep } from "lodash";
import { Component, Interaction } from "models";
import { UnitMeasurement } from "models/UnitMeasurement";
import moment from "moment";
import { deviceComponentID } from "pbHelpers/Component";
import {
extension,
//getComponentVisual,
GetNumNodes,
GetNumNodesFromUnitMeasurement,
GraphFilters
} from "pbHelpers/ComponentType";
import { binSplitAt, isBinSplit } from "pbHelpers/ComponentTypes";
import { findInteractionsAsSource } from "pbHelpers/Interaction";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useGlobalState } from "providers";
import React, { useEffect, useState } from "react";
import { or } from "utils/types";
import UnitMeasurementSummary from "./UnitMeasurementSummary";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => {
return ({
borderedContainer: {
padding: theme.spacing(1),
marginBottom: theme.spacing(2),
border: "1px solid rgba(255, 255, 255, 0.12)",
borderRadius: "4px"
},
container: {
padding: theme.spacing(1),
marginBottom: theme.spacing(2)
},
grainNode: {
color: lightGreen[700]
},
airNode: {
color: theme.palette.text.primary
},
divider: {
margin: theme.spacing(1)
},
fullWidth: {
width: "100%"
}
})
});
interface Props {
deviceKey: string;
componentKey: string;
component: Component;
interactions: Interaction[];
sampling: boolean;
samples?: pond.Measurement[]; //deprecated: old measurement structure
unitMeasurements?: UnitMeasurement[];
sampleLimit?: number;
updateDate?: (startDate: any, endDate: any, live?: boolean) => void;
allowLive?: boolean;
recentMeasurement?: pond.Measurement;
recentUnitMeasurement?: UnitMeasurement[];
deviceComponentPreferences?: pond.DeviceComponentPreferences;
}
function setComponentGraphRanges(device: number | string, component: string, ranges: Range[]) {
const key: string = "graphRanges-" + deviceComponentID(device, component);
localStorage.setItem(key, JSON.stringify(or(ranges, [])));
}
function getComponentGraphRanges(device: number | string, component: string): Range[] {
const key: string = "graphRanges-" + deviceComponentID(device, component);
return JSON.parse(or(localStorage.getItem(key), "[]"));
}
export default function ComponentChart(props: Props) {
const { component, deviceComponentPreferences } = props;
const classes = useStyles();
const isMobile = useMobile();
const { deviceKey, componentKey } = props;
const prevDevice = usePrevious(props.deviceKey);
const prevComponentKey = usePrevious(props.componentKey);
const prevComponent = usePrevious(props.component);
const defaultDateRange = GetDefaultDateRange();
const [startDate, setStartDate] = useState(defaultDateRange.start);
const [endDate, setEndDate] = useState(defaultDateRange.end);
const [live, setLive] = useState(defaultDateRange.live);
const [graphFilters, setGraphFilters] = useState<GraphFilters>({
selectedNodes: ["all"],
selectedInteractions: [],
selectedOverlays: [],
grainType: pond.Grain.GRAIN_NONE
});
// const [{ newStructure }] = useGlobalState();
const [expandOverlays, setExpandOverlays] = useState(false);
const [expandInteractions, setExpandInteractions] = useState(false);
const [expandData, setExpandData] = useState(false);
const [showOriginal, setShowOriginal] = useState(true);
const [showAveraged, setShowAveraged] = useState(false);
useEffect(() => {
if (prevComponent !== props.component) {
let updatedFilters = cloneDeep(graphFilters);
let filledTo = props.component.settings.grainFilledTo;
updatedFilters.grainFilledTo = filledTo > 0 ? filledTo : undefined;
updatedFilters.grainType = props.component.settings.grainType;
setGraphFilters(updatedFilters);
}
}, [prevComponent, graphFilters, props.component]);
useEffect(() => {
if (prevDevice !== deviceKey || prevComponentKey !== componentKey) {
const ranges = getComponentGraphRanges(deviceKey, componentKey);
let updatedFilters = cloneDeep(graphFilters);
updatedFilters.ranges = ranges;
setGraphFilters(updatedFilters);
}
}, [deviceKey, prevDevice, prevComponentKey, componentKey, graphFilters]);
//use the excluded nodes to set the default for the graph filters
useEffect(() => {
let excludedNodes = deviceComponentPreferences?.excludedNodes;
if (excludedNodes === undefined || excludedNodes.length === 0) return;
let updatedFilters = cloneDeep(graphFilters);
if (excludedNodes && component.lastMeasurement[0]) {
let includedNodes: string[] = [];
let numNodes = GetNumNodesFromUnitMeasurement(
UnitMeasurement.any(component.lastMeasurement[0])
);
for (let i = 0; i < numNodes; i++) {
if (!excludedNodes.includes(i)) {
includedNodes.push(i.toString());
}
}
updatedFilters.selectedNodes = includedNodes;
}
setGraphFilters(updatedFilters);
}, [deviceComponentPreferences, component]); //eslint-disable-line react-hooks/exhaustive-deps
const updateDateRange = (newStartDate: any, newEndDate: any, live: boolean) => {
setStartDate(newStartDate);
setEndDate(newEndDate);
setLive(live);
if (props.updateDate) {
props.updateDate(newStartDate, newEndDate, live);
}
};
const changeRange = (index: number, range: Range) => {
let updatedFilters = cloneDeep(graphFilters);
let ranges = updatedFilters.ranges ? updatedFilters.ranges : [];
ranges[index] = range;
updatedFilters.ranges = ranges;
setComponentGraphRanges(deviceKey, componentKey, ranges);
setGraphFilters(updatedFilters);
};
const handleGrainCableControlsChange = (name: string) => (event: any) => {
let updatedFilters = cloneDeep(graphFilters);
let selectedNodes = updatedFilters.selectedNodes ? updatedFilters.selectedNodes : ["all"];
if (event.target.checked) {
if (name === "all" || name === "grain" || name === "air") {
selectedNodes = [name];
} else {
selectedNodes = selectedNodes.filter(item => {
let str = item.toString();
return str !== "all" && str !== "air" && str !== "grain";
});
selectedNodes.push(name);
}
} else {
if (selectedNodes.includes(name)) {
selectedNodes = selectedNodes.filter(item => item.toString() !== name.toString());
if (selectedNodes.length < 1 && name !== "all") {
selectedNodes = ["all"];
}
}
}
updatedFilters.selectedNodes = selectedNodes;
setGraphFilters(updatedFilters);
};
const cableControls = () => {
const { component, samples, deviceComponentPreferences } = props;
const excludedNodes = deviceComponentPreferences?.excludedNodes ?? [];
const selectedNodes = graphFilters.selectedNodes ? graphFilters.selectedNodes : ["all"];
const lastMeasurement = component.status.lastMeasurement;
let numNodes = 0;
if (samples) {
numNodes = GetNumNodes(
component.settings.type,
lastMeasurement && lastMeasurement.measurement
);
} else {
if (component.lastMeasurement && component.lastMeasurement[0]) {
numNodes = GetNumNodesFromUnitMeasurement(
UnitMeasurement.any(component.lastMeasurement[0])
);
}
}
if (numNodes <= 1) {
return null;
}
let grainCableControls = [];
let split = isBinSplit(graphFilters);
let splitAt = binSplitAt(graphFilters);
for (var i = numNodes; i > 0; i--) {
if (i === numNodes) {
grainCableControls.push(
<Grid size={{ xs: 12 }} key="group-selectors">
<FormControlLabel
control={
<Checkbox
checked={selectedNodes.includes("all") ? true : false}
onChange={handleGrainCableControlsChange("all")}
value={"all"}
disabled={selectedNodes.includes("all") ? true : false}
/>
}
label={"All"}
/>
{split && (
<FormControlLabel
control={
<Checkbox
checked={selectedNodes.includes("grain") ? true : false}
onChange={handleGrainCableControlsChange("grain")}
value={"grain"}
disabled={selectedNodes.includes("grain") ? true : false}
/>
}
label={
// newStructure ? (
<Typography variant="body2" className={classes.grainNode}>
{graphFilters.grainType ? grainName(graphFilters.grainType) : "Grain"}
</Typography>
// ) : (
// <Typography className={classes.grainNode}>
// {graphFilters.grainType ? grainName(graphFilters.grainType) : "Grain"}
// </Typography>
// )
}
/>
)}
{split && (
<FormControlLabel
control={
<Checkbox
checked={selectedNodes.includes("air") ? true : false}
onChange={handleGrainCableControlsChange("air")}
value={"air"}
disabled={selectedNodes.includes("air") ? true : false}
/>
}
label={<Typography variant="body2">Air</Typography>}
/>
)}
<Divider key="divider" />
</Grid>
);
}
let nodeLabel = "Node " + i.toString();
if (i === 1) {
nodeLabel = "Bottom";
} else if (i === numNodes) {
nodeLabel = "Top";
}
//TODO-DRAGER: this mey not be necessary to do anymore with them being split up
if (component.type() === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) {
let describer = describeMeasurement(
component.lastMeasurement[0].type,
component.type(),
component.subType()
);
let details = describer.nodeDetails();
if (details) {
nodeLabel = details.labels[i - 1];
}
}
grainCableControls.push(
<Grid size={{ xs: 6 }} key={"node-" + (i - 1).toString()}>
<FormControlLabel
className={split && i <= splitAt ? classes.grainNode : classes.airNode}
control={
<Checkbox
checked={selectedNodes.includes((i - 1).toString()) ? true : false}
onChange={handleGrainCableControlsChange((i - 1).toString())}
value={(i - 1).toString()}
disabled={excludedNodes.includes(i - 1)}
/>
}
label={<Typography variant="body2">{nodeLabel}</Typography>}
/>
</Grid>
);
}
return numNodes > 0 ? (
<Card raised style={{ padding: 25, marginBottom: 20 }}>
<Typography variant="body2" style={{ fontWeight: 650 }}>
Node Control
</Typography>
<FormGroup>
<Grid container direction="row">
{grainCableControls}
</Grid>
</FormGroup>
</Card>
) : null;
};
const componentTypeControls = () => {
const { component } = props;
const ext = extension(component.settings.type, component.settings.subtype);
if (ext.isArray) {
return cableControls();
}
return null;
};
const updateInteractions = (i: Interaction) => {
let updatedFilters = cloneDeep(graphFilters);
let selectedInteractions = updatedFilters.selectedInteractions
? updatedFilters.selectedInteractions
: [];
if (selectedInteractions.includes(i.key())) {
selectedInteractions.splice(selectedInteractions.indexOf(i.key()), 1);
} else {
selectedInteractions.push(i.key());
}
updatedFilters.selectedInteractions = selectedInteractions;
setGraphFilters(updatedFilters);
};
const interactionControls = () => {
const { component, interactions } = props;
let componentID: quack.ComponentID = quack.ComponentID.fromObject({
address: component.settings.address,
addressType: component.settings.addressType,
type: component.settings.type
});
let sourceInteractions = findInteractionsAsSource(componentID, interactions);
const selectedInteractions = graphFilters.selectedInteractions
? graphFilters.selectedInteractions
: [];
//create checkboxes for each interaction and as they are checked/unchecked add/remove them from the interactions passed to getComponentVisual
return interactions.length > 0 ? (
<Card raised className={classes.container}>
<Accordion
elevation={0}
expanded={expandInteractions}
onChange={(_, expanded) => {
setExpandInteractions(expanded);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="body2" style={{ fontWeight: 650 }}>
Interactions
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container>
{sourceInteractions.map((interaction, i) => (
<Grid size={{ xs: 12 }} key={"interaction " + i}>
<FormControlLabel
control={
<Checkbox
checked={selectedInteractions.includes(interaction.key())}
onChange={() => updateInteractions(interaction)}
value={interaction}
/>
}
label={<Typography variant="body2">Interaction {i + 1}</Typography>}
/>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
</Card>
) : null;
// return interactions.length > 0 ? (
// <FormControl className={classes.borderedContainer}>
// <FormLabel component="legend">Interactions</FormLabel>
// <FormGroup>
// <Grid container direction="column">
// {sourceInteractions.map((interaction, i) => (
// <Grid item xs={12} key={"interaction " + i}>
// <FormControlLabel
// control={
// <Checkbox
// checked={selectedInteractions.includes(interaction.key())}
// onChange={() => updateInteractions(interaction)}
// value={interaction}
// />
// }
// label={"Interaction " + (i + 1)}
// />
// </Grid>
// ))}
// </Grid>
// </FormGroup>
// </FormControl>
// ) : null;
};
const updateOverlays = (overlayIndex: number) => {
let updatedFilters = cloneDeep(graphFilters);
// let selectedInteractions = updatedFilters.selectedInteractions
// ? updatedFilters.selectedInteractions
// : [];
let selectedOverlays = updatedFilters.selectedOverlays ?? [];
if (selectedOverlays.includes(overlayIndex.toString())) {
selectedOverlays.splice(selectedOverlays.indexOf(overlayIndex.toString()), 1);
} else {
selectedOverlays.push(overlayIndex.toString());
}
updatedFilters.selectedOverlays = selectedOverlays;
setGraphFilters(updatedFilters);
};
const overlayControls = () => {
const { component } = props;
// let componentID: quack.ComponentID = quack.ComponentID.fromObject({
// address: component.settings.address,
// addressType: component.settings.addressType,
// type: component.settings.type
// });
let overlays = component.settings.overlays;
if (overlays.length === 0) return;
const selectedOverlays = graphFilters.selectedOverlays ?? [];
// if (newStructure) {
return overlays.length > 0 ? (
<Card raised className={classes.container}>
<Accordion
elevation={0}
expanded={expandOverlays}
onChange={(_, expanded) => {
setExpandOverlays(expanded);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="body2" style={{ fontWeight: 650 }}>
Overlays
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container>
{overlays.map((overlay, i) => (
<Grid size={{ xs: 12 }} key={"overlay:" + i}>
<FormControl fullWidth>
<FormControlLabel
classes={{
label: classes.fullWidth
}}
control={
<Checkbox
checked={selectedOverlays.includes(i.toString())}
onChange={() => updateOverlays(i)}
value={overlay.message}
/>
}
label={
<Grid container direction="row" justifyContent="space-between">
<Grid >
<Typography variant="body2">{overlay.message}</Typography>
</Grid>
<Grid>
<Box
width="20px"
height="20px"
borderRadius="50%"
//className={classes.on}
style={{ background: overlay.colour }}
/>
</Grid>
</Grid>
}
/>
</FormControl>
</Grid>
))}
</Grid>
</AccordionDetails>
</Accordion>
</Card>
) : null;
// }
// return overlays.length > 0 ? (
// <FormControl className={classes.borderedContainer}>
// <FormLabel component="legend">Overlays</FormLabel>
// <FormGroup>
// <Grid container direction="column">
// {overlays.map((overlay, i) => (
// <Grid item xs={12} key={"overlay:" + i}>
// <FormControlLabel
// control={
// <Checkbox
// checked={selectedOverlays.includes(i.toString())}
// onChange={() => updateOverlays(i)}
// value={overlay.message}
// />
// }
// label={overlay.message}
// />
// </Grid>
// ))}
// </Grid>
// </FormGroup>
// </FormControl>
// ) : null;
};
const dataDisplayControls = () => {
const { unitMeasurements } = props;
return (
<Card raised className={classes.container}>
<Accordion
elevation={0}
expanded={expandData}
onChange={(_, expanded) => {
setExpandData(expanded);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant="body2" style={{ fontWeight: 650 }}>
Data Display Options
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container style={{ width: "100%" }}>
<Grid size={{ xs: 12 }}>
<FormControl fullWidth>
<FormControlLabel
classes={{
label: classes.fullWidth
}}
control={
<Checkbox
checked={showOriginal}
onChange={(_, checked) => {
setShowOriginal(checked);
}}
value={showOriginal}
/>
}
label={
<Grid container direction="row" justifyContent="space-between" wrap="nowrap">
<Grid >
<Typography variant="body2">OriginalData</Typography>
</Grid>
<Grid >
<Grid container spacing={1}>
{unitMeasurements?.map((um, i) => (
<Grid key={i}>
<Box
width="20px"
height="20px"
borderRadius="50%"
//className={classes.on}
style={{ background: describeMeasurement(um.type).colour() }}
/>
</Grid>
))}
</Grid>
</Grid>
</Grid>
}
/>
</FormControl>
</Grid>
<Grid size={{ xs: 12 }}>
<FormControl fullWidth>
<FormControlLabel
classes={{
label: classes.fullWidth
}}
control={
<Checkbox
checked={showAveraged}
onChange={(_, checked) => {
setShowAveraged(checked);
}}
value={showAveraged}
/>
}
label={
<Grid container direction="row" justifyContent="space-between">
<Grid >
<Typography variant="body2">Averaged Data</Typography>
</Grid>
<Grid >
<Box
width="20px"
height="20px"
borderRadius="50%"
//className={classes.on}
style={{ background: "white" }}
/>
</Grid>
</Grid>
}
/>
</FormControl>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</Card>
);
};
const getGraph = () => {
const { component, interactions, unitMeasurements, sampleLimit } = props;
// if (samples && samples.length > 1) {
// //TODO: is being deprecated
// return getComponentVisual(
// component.settings.type,
// component.settings.subtype,
// samples,
// interactions,
// component.settings.overlays,
// GraphOrientation.GRAPH_ORIENTATION_LANDSCAPE,
// graphFilters
// );
// } else
if (unitMeasurements) {
return (
<React.Fragment>
<MeasurementsChart
startDate={startDate}
endDate={endDate}
component={component}
interactions={interactions}
unitMeasurements={unitMeasurements}
sampleLimit={sampleLimit}
filters={graphFilters}
updateDateRange={updateDateRange}
showOriginal={showOriginal}
showAveraged={showAveraged}
/>
</React.Fragment>
);
} else {
return (
<React.Fragment>
<Typography variant="h6" color="textPrimary" align="center">
Not enough data to display
</Typography>
<Typography variant="subtitle2" color="textSecondary" align="center">
{live
? "(Waiting to receive a few live measurements)"
: "(Queried between " +
moment(startDate).calendar() +
" and " +
moment(endDate).calendar() +
")"}
</Typography>
</React.Fragment>
);
}
};
const content = () => {
const {
component,
sampling,
samples,
recentMeasurement,
recentUnitMeasurement,
deviceComponentPreferences
} = props;
const ext = extension(component.settings.type, component.settings.subtype);
const ranges = graphFilters.ranges ? graphFilters.ranges : [];
let convertedLastMeasurement;
if (recentUnitMeasurement && recentUnitMeasurement.length > 0) {
let measurements = recentUnitMeasurement;
convertedLastMeasurement = UnitMeasurement.convertLastMeasurement(measurements);
}
return (
<Grid container spacing={2} direction={isMobile ? "column" : "row-reverse"}>
{isMobile && (
<Grid className={classes.borderedContainer}>
{samples ? (
<MeasurementSummary component={component} reading={recentMeasurement} />
) : (
<Card raised className={classes.container}>
<Typography style={{ fontWeight: 650 }}>Latest Measurements:</Typography>
<UnitMeasurementSummary
component={component}
reading={convertedLastMeasurement}
excludedNodes={deviceComponentPreferences?.excludedNodes}
/>
</Card>
)}
</Grid>
)}
<Grid
size={{
xs: 12,
sm: 7,
md: 8,
lg: 9,
xl: 10
}}
container
direction="column"
justifyContent="center"
alignItems="center">
{sampling ? <CircularProgress /> : <React.Fragment>{getGraph()}</React.Fragment>}
</Grid>
<Grid size={{ xs: 12, sm: 5, md: 4, lg: 3, xl: 2 }} container alignContent="flex-start">
{!isMobile && (
<React.Fragment>
{/* {newStructure && ( */}
<Grid size={{ xs: 12 }}>
<Typography style={{ fontWeight: 650, fontSize: 20 }}>Status</Typography>
<Divider variant="middle" className={classes.divider} />
</Grid>
{/* )} */}
<Grid size={{ xs: 12 }} className={classes.borderedContainer}>
{samples ? (
<MeasurementSummary component={component} reading={recentMeasurement} />
) : (
<Card raised style={{ padding: 25, marginBottom: 20 }}>
<Typography style={{ fontWeight: 650 }}>Latest Measurements:</Typography>
<UnitMeasurementSummary
component={component}
reading={convertedLastMeasurement}
largeText
boldMeasurements
excludedNodes={deviceComponentPreferences?.excludedNodes}
/>
</Card>
)}
</Grid>
</React.Fragment>
)}
{!isMobile && (
<Grid size={{ xs: 12 }}>
<Typography style={{ fontWeight: 650, fontSize: 20 }}>Graph Controls</Typography>
<Divider variant="middle" className={classes.divider} />
</Grid>
)}
<Grid size={{ xs: 12 }}>
{componentTypeControls()}
</Grid>
<Grid size={{ xs: 12 }}>
{interactionControls()}
</Grid>
<Grid size={{ xs: 12 }}>
{overlayControls()}
</Grid>
{component.settings.smoothingAverages !== 0 && (
<Grid size={{ xs: 12 }}>
{dataDisplayControls()}
</Grid>
)}
{!sampling &&
ext.measurements.map((m, i) => {
const range = ranges[i] ? ranges[i] : ({} as Range);
if (m.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN) {
return null;
}
if (samples) {
return (
<Grid size={{ xs: 12 }} key={"range-" + i}>
<RangeInput
range={range}
onChange={(updatedRange: Range) => changeRange(i, updatedRange)}
label={m.label + " Range"}
/>
</Grid>
);
}
return null;
})}
</Grid>
</Grid>
);
};
return (
<Card>
<CardContent>{content()}</CardContent>
</Card>
);
}

View file

@ -0,0 +1,266 @@
import {
Button,
Card,
CardContent,
CardHeader,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Theme,
Typography
} from "@mui/material";
import { Component, Device } from "models";
import moment from "moment";
import { getHumanReadableAddress } from "pbHelpers/AddressType";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { useComponentAPI } from "providers";
import React, { useEffect, useState } from "react";
// import { getThemeType } from "theme";
import AddCompsFromDiag from "./AddCompsFromDiag";
import { getThemeType } from "theme/themeType";
import { makeStyles } from "@mui/styles";
interface Props {
component: Component;
device: Device;
refreshCallback: () => void;
//not worrying about permissions for this component as this would only be rendered if the user has write permissions to the device
//permissions: pond.Permission[]
}
export interface CableInfo {
id: number;
subtype: quack.GrainCableSubtype | undefined;
name: string;
nodeCount: number;
}
const useStyles = makeStyles((theme: Theme) => {
return ({
cardHeader: {
padding: theme.spacing(1),
paddingLeft: theme.spacing(2)
},
cell: {
padding: 5
},
dark: {
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)",
padding: 0
},
light: {
backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)",
padding: 0
}
})
});
export default function ComponentDiagnostics(props: Props) {
const { component, device, refreshCallback } = props;
const [portNumber, setPortNumber] = useState("");
const compAPI = useComponentAPI();
const classes = useStyles();
const [cableInfoList, setCableInfoList] = useState<CableInfo[]>([]);
const [measurements, setMeasurements] = useState<pond.UnitMeasurementsForComponent[]>([]);
//const { error, success } = useSnackbar();
const [loadingMeasurements, setLoadingMeasurements] = useState(false);
const [openAdd, setOpenAdd] = useState(false);
useEffect(() => {
setPortNumber(
getHumanReadableAddress(
component.settings.addressType,
component.settings.type,
component.settings.address,
device.settings.product
)
);
//get the last 10 measurements that occured over a 24 hour period
if (!loadingMeasurements) {
setLoadingMeasurements(true);
compAPI
.listUnitMeasurements(
device.id(),
component.key(),
moment()
.subtract(1, "hours")
.toISOString(),
moment().toISOString(),
2,
0,
"desc"
)
.then(resp => {
setMeasurements(resp.data.measurements);
})
.finally(() => {
setLoadingMeasurements(false);
});
}
}, [component, device, compAPI]); //eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
let cableIDs: number[] = [];
let cableInfo: CableInfo[] = [];
if (measurements) {
measurements.forEach(UnitMeasurement => {
let unitM = pond.UnitMeasurementsForComponent.fromObject(UnitMeasurement);
if (unitM.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) {
if (unitM.values[0] && unitM.values[0].values) {
cableIDs = unitM.values[0].values;
}
} else if (unitM.type === quack.MeasurementType.MEASUREMENT_TYPE_PERCENT) {
unitM.values.forEach(ValArray => {
ValArray.values.forEach((hum, i) => {
if (cableIDs[i]) {
let newInfo = cableFromHumidity(cableIDs[i], hum);
if (cableInfo[i] === undefined) {
//for the first round just put them in
cableInfo.push(newInfo);
} else {
//if something is there do a check to see if they match
if (cableInfo[i].subtype !== newInfo.subtype) {
//if they dont match determine if the newInfo has priority
if (
newInfo.subtype === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_NONE ||
newInfo.subtype === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_SHT35
) {
//if it does then set the V# to be the cable info otherwise leave it alone
cableInfo[i] = newInfo;
}
}
}
}
});
});
}
});
}
setCableInfoList(cableInfo);
}, [measurements]);
const cableFromHumidity = (cableID: number, humidity: number) => {
let nodeCoeff = 0;
let cableInfo: CableInfo = { id: cableID, subtype: undefined, name: "Unknown", nodeCount: 0 };
if (humidity >= 1 && humidity <= 16) {
cableInfo.subtype = quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_NONE;
cableInfo.name = "Moisture Cable";
nodeCoeff = 0;
} else if (humidity >= 17 && humidity <= 32) {
cableInfo.subtype = quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_FROG;
cableInfo.name = "Adaptive Sensor";
nodeCoeff = 1;
} else if (humidity >= 33 && humidity <= 48) {
cableInfo.subtype = quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP;
cableInfo.name = "Temp Only Cable";
nodeCoeff = 2;
} else if (humidity >= 49) {
cableInfo.subtype = quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_SHT35;
cableInfo.name = "Moisture Cable V2";
nodeCoeff = 3;
}
cableInfo.nodeCount = humidity - 16 * nodeCoeff;
return cableInfo;
};
// deprecated: this functionality has moved to the AddCompsFromDiag component
// const addCablesToDevice = () => {
// let componentsToAdd: pond.MultiComponentSettings = pond.MultiComponentSettings.create();
// cableInfoList.forEach(cable => {
// let settings: pond.ComponentSettings = pond.ComponentSettings.create();
// settings.type = quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE;
// settings.subtype = cable.subtype ?? quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_NONE;
// settings.addressType = cable.id + 8;
// settings.address = component.settings.address;
// settings.measurementPeriodMs = 3600000; //set the measurement period to 1 hour
// settings.reportPeriodMs = 3600000;
// settings.name = cable.name;
// componentsToAdd.components.push(settings);
// });
// compAPI
// .addMultiComponents(device.id(), componentsToAdd)
// .then(resp => {
// success("Components added to Device");
// })
// .catch(err => {
// error("One or more component failed to add");
// })
// .finally(() => {
// refreshCallback();
// });
// };
return (
<React.Fragment>
<Card raised>
<CardHeader
title={<Typography variant="body1">Port {portNumber} Auto Detection</Typography>}
className={classes.cardHeader}
/>
<CardContent style={{ paddingTop: 0 }}>
{measurements && measurements.length > 0 && (
<React.Fragment>
<Table>
<TableHead>
<TableRow>
<TableCell align="center" className={classes.cell}>
Cable ID
</TableCell>
<TableCell align="center" className={classes.cell}>
Cable Type
</TableCell>
<TableCell align="center" className={classes.cell}>
Nodes
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{cableInfoList.map((cable, i) => (
<TableRow className={i % 2 === 0 ? classes.light : classes.dark} key={i}>
<TableCell align="center" className={classes.cell}>
{cable.id}
</TableCell>
<TableCell align="center" className={classes.cell}>
{cable.name}
</TableCell>
<TableCell align="center" className={classes.cell}>
{cable.nodeCount}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{cableInfoList.length > 0 && (
<Button
style={{ marginTop: 20 }}
variant="contained"
color="primary"
onClick={() => {
//addCablesToDevice();
setOpenAdd(true);
}}>
Add to Device
</Button>
)}
</React.Fragment>
)}
</CardContent>
</Card>
<AddCompsFromDiag
cableInfo={cableInfoList}
port={portNumber}
device={device}
diagComponent={component}
open={openAdd}
closeDialog={() => {
setOpenAdd(false);
}}
refreshCallback={refreshCallback}
/>
</React.Fragment>
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,69 @@
import { Box } from "@material-ui/core";
import { useComponentAPI } from "hooks";
import { Component, Device } from "models";
import { pond } from "protobuf-ts/pond";
import React from "react";
import { or } from "utils/types";
import { TranslateKey, TranslateValue } from "pbHelpers/Component";
import DiffHistory, { ListResult, Record } from "common/DiffHistory";
interface Props {
device: Device;
component: Component;
}
export default function ComponentHistory(props: Props) {
const componentAPI = useComponentAPI();
let list = (limit: number, offset: number): Promise<ListResult> => {
return new Promise(resolve => {
componentAPI
.listHistory(props.device.id(), props.component.key(), limit, offset)
.then((res: any) => {
let records: Record[] = or(res.data.history, []).map((record: any) => {
return {
timestamp: or(record.timestamp, ""),
user: or(record.user, ""),
data: or(record.component, {}),
status: or(record.progress, "Unknown")
} as Record;
});
resolve({
records: records,
total: or(res.data.total, 0),
offset: or(res.data.nextOffset, 0)
});
})
.catch((err: any) => {
resolve({
records: [] as Record[],
total: 0,
offset: 0
});
});
});
};
let translateKey = (key: keyof any): string => {
return TranslateKey(key as keyof pond.ComponentSettings);
};
let translateValue = (key: keyof any, obj: any): string => {
return TranslateValue(
key as keyof pond.ComponentSettings,
pond.ComponentSettings.fromObject(obj)
);
};
return (
<Box>
<DiffHistory
name={props.component.name()}
kind="component"
list={list}
translateKey={translateKey}
translateValue={translateValue}
showTitle={true}
/>
</Box>
);
}

View file

@ -0,0 +1,139 @@
[
{
"type": 12,
"subtype": 0,
"mode": {
"labelA": "Power Mode",
"entryA": "select",
"dictionaryA": [
{
"key": "High Power",
"value": 1,
"restricted": false
},
{
"key": "Low Power",
"value": 2,
"restricted": false
},
{
"key": "Calibration",
"value": 3,
"restricted": true
}
],
"labelB": "Number of Averages",
"entryB": "number",
"dictionaryB": [
{
"key": "min",
"value": 1,
"restricted": false
},
{
"key": "max",
"value": 8,
"restricted": false
}
]
}
},
{
"type": 28,
"subtype": 0,
"mode": {
"labelA": "Power Mode",
"entryA": "select",
"dictionaryA": [
{
"key": "High Power",
"value": 1,
"restricted": false
},
{
"key": "Low Power",
"value": 2,
"restricted": false
}
],
"labelB": "Number of Averages",
"entryB": "number",
"minB": 1,
"maxB": 8
}
},
{
"type": 5,
"subtype": 5,
"mode": {
"labelA": "From Node Id",
"entryA": "number",
"dictionaryA": [
{
"key": "min",
"value": 1,
"restricted": false
},
{
"key": "max",
"value": 100,
"restricted": false
}
],
"labelB": "To Node Id",
"entryB": "number",
"dictionaryB": [
{
"key": "min",
"value": 1,
"restricted": false
},
{
"key": "max",
"value": 100,
"restricted": false
}
]
}
},
{
"type": 5,
"subtype": 0,
"mode": {
"labelA": "Read Mode",
"entryA": "select",
"dictionaryA": [
{
"key": "Fast Read",
"value": 1,
"restricted": false
},
{
"key": "Standard",
"value": 2,
"restricted": false
}
]
}
},
{
"type": 5,
"subtype": 2,
"mode": {
"labelA": "Read Mode",
"entryA": "select",
"dictionaryA": [
{
"key": "Fast Read",
"value": 1,
"restricted": false
},
{
"key": "Standard",
"value": 2,
"restricted": false
}
]
}
}
]

View file

@ -0,0 +1,184 @@
import {
Avatar,
Button,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
ListItemAvatar,
ListItemIcon,
ListItemText,
ListSubheader,
Typography
} from "@material-ui/core";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import DragIcon from "@material-ui/icons/DragHandle";
import ResponsiveDialog from "common/ResponsiveDialog";
import { useDeviceAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { Component, Device } from "models";
import { sortComponents } from "pbHelpers/Component";
import { pond, quack } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
item: {
backgroundColor: "rgba(0,0,0,0)",
"&:hover": {
backgroundColor:
theme.palette.type === "light" ? "rgba(0,0,0,0.07)" : "rgba(255,255,255,0.07)"
}
}
})
);
interface ItemProps {
component: Component;
index: number;
}
function ComponentItem(props: ItemProps) {
const classes = useStyles();
const { component, index } = props;
const name = component.name();
return (
<Draggable draggableId={component.key()} index={index}>
{provided => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={classes.item}>
<ListItemIcon>
<DragIcon />
</ListItemIcon>
<ListItemText primary={<Typography color="textPrimary">{name}</Typography>} />
<ListItemAvatar>
<Avatar alt={name}>{index + 1}</Avatar>
</ListItemAvatar>
</ListItem>
)}
</Draggable>
);
}
interface ListProps {
open: boolean;
close: (refresh: boolean) => void;
device: Device;
components: Component[];
devicePreferences: pond.UserPreferences;
}
const filteredComponents = (components: Component[]) => {
const componentBlacklist = [
quack.ComponentType.COMPONENT_TYPE_POWER,
quack.ComponentType.COMPONENT_TYPE_MODEM
];
return components.filter(c => !componentBlacklist.includes(c.settings.type));
};
export default function ComponentOrder(props: ListProps) {
const { open, close, device, devicePreferences } = props;
const deviceAPI = useDeviceAPI();
const { error, success } = useSnackbar();
const prevDisplayOrder = usePrevious(devicePreferences.childDisplayOrder);
const [components, setComponents] = useState<Component[]>(
Array.from(filteredComponents(props.components).values()).sort((a, b: Component) =>
sortComponents(a, b, devicePreferences.childDisplayOrder)
)
);
useEffect(() => {
if (
props.components.length !== components.length ||
prevDisplayOrder !== devicePreferences.childDisplayOrder
) {
setComponents(
Array.from(filteredComponents(props.components).values()).sort((a, b: Component) =>
sortComponents(a, b, devicePreferences.childDisplayOrder)
)
);
}
}, [props.components, prevDisplayOrder, devicePreferences.childDisplayOrder, components.length]);
const updateComponentOrder = (componentOrder: Component[]) => {
let updatedPreferences = cloneDeep(devicePreferences);
updatedPreferences.childDisplayOrder = componentOrder.map(c => c.locationString());
deviceAPI
.updatePreferences(device.id(), updatedPreferences)
.then(() => success("Successfully update the component display order"))
.catch(() => {
error("Error occured while updating the component display order");
})
.finally(() => close(true));
};
const submit = () => {
updateComponentOrder(components);
};
const reorder = (components: Component[], startIndex: number, endIndex: number): Component[] => {
const result = Array.from(components);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const onDragEnd = (result: DropResult) => {
if (!result.destination) {
return;
}
setComponents(reorder(components, result.source.index, result.destination.index));
};
return (
<ResponsiveDialog open={open} onClose={() => close(false)} maxWidth="sm" fullWidth>
<DialogTitle id="component-order-dialog-title">
Component Display Order
<Typography variant="body2" color="textSecondary">
{device.name()}
</Typography>
</DialogTitle>
<DialogContent>
{components.length < 1 ? (
<DialogContentText>No components to reorder</DialogContentText>
) : (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="list">
{provided => (
<List
ref={provided.innerRef}
{...provided.droppableProps}
subheader={
<ListSubheader disableSticky>
<Typography variant="body2" color="textSecondary">
Drag and Drop to change the display order
</Typography>
</ListSubheader>
}>
{components.map((component, index) => (
<ComponentItem key={component.key()} component={component} index={index} />
))}
{provided.placeholder}
</List>
)}
</Droppable>
</DragDropContext>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => close(false)} color="primary">
Close
</Button>
<Button onClick={submit} color="primary">
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
import {
Chip,
Tooltip,
makeStyles,
createStyles,
Theme,
Avatar,
useTheme
} from "@material-ui/core";
import { purple } from "@material-ui/core/colors";
import DefaultIcon from "@material-ui/icons/Memory";
import { getDescription, GetComponentIcon } from "pbHelpers/ComponentType";
import React from "react";
import { Component } from "models";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
icon: {
color: purple["500"]
},
customIcon: {
height: theme.spacing(2.5),
width: theme.spacing(2.5)
}
})
);
interface Props {
component: Component;
}
export default function ComponentTypeChip(props: Props) {
const { component } = props;
const theme = useTheme<Theme>();
const { type, subtype } = component.settings;
const classes = useStyles();
const icon = () => {
let componentIcon = GetComponentIcon(type, subtype, theme.palette.type);
return componentIcon ? (
<Avatar
variant="square"
src={componentIcon}
className={classes.customIcon}
alt={"type:" + type + "-subtype:" + subtype}
/>
) : (
<DefaultIcon className={classes.icon} />
);
};
if (component.settings.type) {
return (
<Tooltip title={getDescription(component.settings.type)}>
<Chip variant="outlined" label={component.name()} icon={icon()} />
</Tooltip>
);
} else {
return null;
}
}

View file

@ -0,0 +1,414 @@
import {
Button,
CircularProgress,
createStyles,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Grid,
InputAdornment,
TextField,
Typography
} from "@material-ui/core";
import { Theme } from "@material-ui/core/styles/createMuiTheme";
import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles";
import DateSelect from "common/time/DateSelect";
import { Component, Device, User } from "models";
import moment, { Moment } from "moment";
import { ComponentMeasurement, getMeasurements } from "pbHelpers/ComponentType";
import { pond } from "protobuf-ts/pond";
import React from "react";
import { downloadJSON, exportDataToCSV } from "utils/download";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { ComponentAPIContext } from "providers/pond/componentAPI";
import { UnitMeasurement } from "models/UnitMeasurement";
const styles = (theme: Theme) =>
createStyles({
loadingContent: {
marginTop: theme.spacing(4)
}
});
interface Props extends WithStyles<typeof styles> {
device: Device;
component: Component;
isDialogOpen: boolean;
closeDialogCallback: Function;
initialStartDate?: Moment;
initialEndDate?: Moment;
isJSON?: boolean;
newMeasurements?: boolean;
user?: User;
}
interface State {
startDate: any;
endDate: any;
isLoading: boolean;
filename: string;
}
class ExportDataSettings extends React.Component<Props, State> {
static contextType = ComponentAPIContext;
context!: React.ContextType<typeof ComponentAPIContext>;
constructor(props: Props) {
super(props);
this.state = this.defaultSettings(props);
}
componentDidUpdate = (prevProps: Props) => {
const { initialStartDate, initialEndDate } = this.props;
if (
prevProps.initialStartDate !== initialStartDate ||
prevProps.initialEndDate !== initialEndDate
) {
this.setState({
startDate: initialStartDate,
endDate: initialEndDate
});
}
};
defaultSettings = (props: Props) => {
const { initialStartDate, initialEndDate } = props;
return {
startDate: initialStartDate ? initialStartDate : moment().subtract(1, "days"),
endDate: initialEndDate ? initialEndDate : moment(),
isLoading: false,
filename: moment()
.utc()
.format()
} as State;
};
close = () => {
this.setState(this.defaultSettings(this.props));
this.props.closeDialogCallback();
};
submit = () => {
const { device, component, newMeasurements, user } = this.props;
const { startDate, endDate } = this.state;
const { sampleMeasurements, listUnitMeasurements } = this.context;
this.setState({ isLoading: true });
if (newMeasurements) {
listUnitMeasurements(
device.id(),
component.key(),
startDate,
endDate,
0,
0,
"desc",
undefined,
[device.id().toString()],
["device"]
)
.then(resp => {
let measurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
this.formatUnitMeasurements(measurements)
.then((data: Array<any>) => {
this.setState({
isLoading: false
});
this.exportData(
data
.map(entry => {
let out = entry;
out.Timestamp = moment(entry.Timestamp)
.local()
.format();
return out;
})
.sort(function(a, b) {
return a.Timestamp < b.Timestamp ? -1 : 1;
})
);
this.close();
})
.catch((error: any) => {
this.setState({ isLoading: false });
this.close();
});
})
.catch((error: any) => {
this.setState({ isLoading: false });
this.close();
});
} else {
sampleMeasurements(device.id(), component.key(), startDate, endDate, 4294967295)
.then((response: any) => {
this.formatMeasurements(response.data.measurements)
.then((data: Array<any>) => {
this.setState({
isLoading: false
});
this.exportData(
data
.map(entry => {
let out = entry;
out.Timestamp = moment(entry.Timestamp)
.local()
.format();
return out;
})
.sort(function(a, b) {
return a.Timestamp < b.Timestamp ? -1 : 1;
})
);
this.close();
})
.catch((error: any) => {
this.setState({ isLoading: false });
this.close();
});
})
.catch((error: any) => {
this.setState({ isLoading: false });
this.close();
});
}
};
// formatExportedMeasurements(measurements: Array<pond.ExportedMeasurement>): Promise<Array<any>> {
// return new Promise(function(resolve, reject) {
// if (!measurements) reject("Invalid measurements");
// let formattedMeasurements: Array<any> = [];
// for (var i = 0; i < measurements.length; i++) {
// let measurement: pond.ExportedMeasurement = measurements[i];
// let lastMeasurement = i === measurements.length - 1;
// if (!measurement || !measurement.timestamp) {
// if (lastMeasurement) {
// resolve(formattedMeasurements);
// }
// } else {
// let row: any = {
// Timestamp: measurement.timestamp,
// MeasurementType: measurement.measurementType,
// MeasurementValue: measurement.measurementValue,
// MeasurementUnit: measurement.measurementUnit
// };
// formattedMeasurements.push(row);
// if (lastMeasurement) {
// resolve(formattedMeasurements);
// }
// }
// }
// });
// }
formatMeasurements(measurements: Array<pond.Measurement>): Promise<Array<any>> {
const { device, component } = this.props;
return new Promise(function(resolve, reject) {
if (!measurements) reject("Invalid measurements");
let formattedMeasurements: Array<any> = [];
for (var i = 0; i < measurements.length; i++) {
let measurement: pond.Measurement = measurements[i];
let lastMeasurement = i === measurements.length - 1;
if (!measurement || !measurement.timestamp || !measurement.measurement) {
if (lastMeasurement) {
resolve(formattedMeasurements);
}
} else {
let row: any = {
Device: device.name(),
Component: component.name(),
Timestamp: measurement.timestamp
};
const componentMeasurements: Array<ComponentMeasurement> = getMeasurements(
component.settings.type
);
componentMeasurements.forEach((componentMeasurement: ComponentMeasurement) => {
let unit = describeMeasurement(
componentMeasurement.measurementType,
component.settings.type,
component.settings.subtype
).unit();
let key = componentMeasurement.label + " (" + unit + ")";
row[key] = componentMeasurement.extract(measurement.measurement);
});
formattedMeasurements.push(row);
if (lastMeasurement) {
resolve(formattedMeasurements);
}
}
}
});
}
formatUnitMeasurements(unitMeasurements: UnitMeasurement[]): Promise<Array<any>> {
const { device, component } = this.props;
return new Promise(function(resolve, reject) {
if (!unitMeasurements) reject("Invalid measurements");
let formattedMeasurements: Map<string, any> = new Map<string, any>();
unitMeasurements.forEach(um => {
let describer = describeMeasurement(
um.type,
component.settings.type,
component.settings.subtype
);
um.values.forEach((vals, i) => {
let timestamp = um.timestamps[i];
let row: any = formattedMeasurements.get(timestamp);
if (row === undefined) {
row = {
Device: device.name(),
Component: component.name(),
Timestamp: timestamp
};
}
let key = describer.label() + "(" + describer.unit() + ")";
let valueObj: any = {};
vals.values.forEach((val, i) => {
valueObj["node" + i] = val;
});
row[key] = valueObj;
formattedMeasurements.set(timestamp, row);
});
});
resolve(Array.from(formattedMeasurements.values()));
});
}
exportJSON(data: Array<any>) {
const { filename } = this.state;
downloadJSON(data, filename + ".json");
}
exportData(data: Array<any>) {
const { filename } = this.state;
exportDataToCSV(filename, data);
}
updateDateRange = (newStartDate: any, newEndDate: any) => {
this.setState({ startDate: newStartDate, endDate: newEndDate });
};
updateFilename = (event: any) => {
this.setState({ filename: event.target.value });
};
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
title() {
const { component } = this.props;
return (
<React.Fragment>
Data Export Settings
<Typography variant="body2" color="textSecondary">
{component.name()}
</Typography>
</React.Fragment>
);
}
content() {
const { classes } = this.props;
const { startDate, endDate, filename, isLoading } = this.state;
if (isLoading) {
return (
<Grid
container
direction="column"
justify="center"
alignItems="center"
className={classes.loadingContent}>
<CircularProgress color="secondary" />
<Typography variant="subtitle1" color="textPrimary">
Generating a custom {this.props.isJSON ? "json..." : "csv..."}
</Typography>
<Typography variant="subtitle2" color="textSecondary">
(This may take a while, please wait)
</Typography>
</Grid>
);
}
return (
<Grid container direction="row" spacing={2}>
<Grid item xs={12}>
<TextField
id="exportDataFilename"
label="Filename"
value={filename}
fullWidth
variant="standard"
onChange={this.updateFilename}
margin="normal"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{this.props.isJSON ? ".json" : ".csv"}
</InputAdornment>
)
}}
/>
</Grid>
<Grid item xs={12}>
<DateSelect
startDate={startDate}
endDate={endDate}
updateDateRange={this.updateDateRange}
label="Extract data from"
/>
</Grid>
<Grid item xs={12}>
<Typography variant="body2" color="textSecondary">
Note: This process might take a while (depends on how much data is requested)
</Typography>
</Grid>
</Grid>
);
}
actions() {
const { isLoading } = this.state;
return (
<React.Fragment>
<span>
<Button onClick={this.close} color="primary">
Cancel
</Button>
<Button onClick={this.submit} color="primary" disabled={isLoading}>
Export
</Button>
</span>
</React.Fragment>
);
}
render() {
return (
<Dialog
fullWidth
open={this.props.isDialogOpen}
onClose={this.close}
aria-labelledby="exportDataSettingsDialog">
<DialogTitle id="exportDataSettingsTitle">{this.title()}</DialogTitle>
<Divider />
<DialogContent>{this.content()}</DialogContent>
<DialogActions>{this.actions()}</DialogActions>
</Dialog>
);
}
}
export default withStyles(styles)(ExportDataSettings);

116
src/component/GPS.tsx Normal file
View file

@ -0,0 +1,116 @@
import { withStyles, Theme, WithTheme, createStyles, WithStyles } from "@material-ui/core/styles";
import React from "react";
import { parseGPS } from "services/google/mapHelpers";
import { geolocate } from "services/google/googleAPI";
import Loader from "common/Loader";
import MapGL from "common/MapGL";
import { pond } from "protobuf-ts/pond";
const styles = (theme: Theme) =>
createStyles({
mapContainer: {
height: "375px",
width: "100%",
padding: theme.spacing(3),
paddingTop: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
height: "475px"
}
}
});
interface Props extends WithStyles<typeof styles>, WithTheme {
data: pond.Measurement[];
}
interface State {
loading: boolean;
coordinates: any[];
}
class GPS extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
loading: false,
coordinates: []
};
}
componentDidMount = () => {
this.parseData();
};
componentDidUpdate = (prevProps: Props) => {
if (this.props.data !== prevProps.data) {
this.parseData();
}
};
parseData = () => {
const { data } = this.props;
this.setState({ loading: true, coordinates: [] });
if (data.length <= 0) {
return;
}
let promises = [];
let timestamps: string[] = [];
let coordinates: any[] = [];
for (let i = 0; i < data.length; i++) {
let e = data[i];
if (e.measurement) {
let ts = e.timestamp;
let gps = e.measurement.gps;
let { request, lat, lng } = parseGPS(gps);
if (request) {
promises.push(geolocate(request));
timestamps.push(ts);
} else if (lat !== 0 && lng !== 0) {
coordinates.push({
timestamp: ts,
latitude: lat,
longitude: lng
});
}
}
}
Promise.all(promises)
.then((responses: any[]) => {
responses.forEach((r, i) => {
coordinates.push({
timestamp: timestamps[i],
latitude: r.data.location.lat,
longitude: r.data.location.lng,
accuracy: r.data.accuracy
});
});
})
.catch((error: any) => {
console.log(error);
})
.finally(() => {
this.setState({ loading: false, coordinates: coordinates });
});
};
render() {
const { coordinates, loading } = this.state;
const { classes } = this.props;
if (loading) {
return <Loader />;
} else {
let paths = new Map().set("path", coordinates);
return (
<div className={classes.mapContainer}>
<MapGL paths={paths} />
</div>
);
}
}
}
export default withStyles(styles, { withTheme: true })(GPS);

View file

@ -0,0 +1,38 @@
import HumidityDarkIcon from "assets/components/humidityDark.png";
import HumidityLightIcon from "assets/components/humidityLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
height?: number;
width?: number;
}
export default function HumidityIcon(props: Props) {
const themeType = useThemeType();
const { type, height, width } = props;
const lightIcon = () => {
return HumidityLightIcon;
};
const darkIcon = () => {
return HumidityDarkIcon;
};
const src = () => {
if (type) {
return type === "light" ? lightIcon() : darkIcon();
}
return themeType === "light" ? darkIcon() : lightIcon();
};
if (height || width) {
return <img alt="tempIcon" src={src()} height={height} width={width} />;
}
return <ImgIcon alt="bins" src={src()} />;
}

View file

@ -0,0 +1,278 @@
import { Box, Grid, Typography, makeStyles, createStyles, Theme } from "@material-ui/core";
import { Component } from "models";
import moment from "moment";
import { getMeasurementSummary, Summary } from "pbHelpers/ComponentType";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
import { or } from "utils/types";
interface Props {
component: Component;
reading?: pond.Measurement | null;
tableCell?: boolean;
centered?: boolean;
dense?: boolean;
omitTime?: boolean;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
"@keyframes ripple": {
from: {
transform: "scale(.8)",
opacity: 1
},
to: {
transform: "scale(1.4)",
opacity: 0
}
},
on: {
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
animationName: "$ripple",
animationDuration: "1.2s",
animationTimingFunction: "ease-in-out",
animationIterationCount: "infinite"
},
off: {
backgroundColor: "transparent"
}
})
);
//TODO: deprecated as unit measurements use a new measurement summary file
export default function MeasurementSummary(props: Props) {
const { component, reading, tableCell, centered, dense } = props;
const [summaries, setSummaries] = useState<Summary[]>([]);
const [timestamp, setTimestamp] = useState("");
const classes = useStyles();
const grainCableFilters = () => {
const { component } = props;
return {
grainType: component.settings.grainType,
grainFilledTo: component.settings.grainFilledTo
};
};
useEffect(() => {
async function getSummary() {
if (
reading &&
reading.measurement &&
reading.measurement.id &&
reading.measurement.id.type !== null &&
reading.measurement.id.type !== undefined
) {
let id = quack.ComponentID.fromObject(reading.measurement.id);
let filters = { isTableCellMode: tableCell };
if (id.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
filters = { ...filters, ...grainCableFilters() };
}
await getMeasurementSummary(
id.type,
or(component.settings.subtype, 0),
quack.Measurement.create(reading.measurement ? reading.measurement : undefined),
filters
).then(summaries => {
setSummaries(summaries);
});
setTimestamp(reading.timestamp);
}
}
getSummary();
}, [component, reading]); // eslint-disable-line react-hooks/exhaustive-deps
const noSummary = () => {
return (
<Typography variant="body2" color="textPrimary">
No Data
</Typography>
);
};
const getSummaryComponent = (summaries: Summary[]): any => {
if (summaries.length < 1) {
return noSummary();
}
return (
<Grid container>
<Grid item xs={12}>
<Typography variant="body2" color="textPrimary">
{summaries.map((summary: Summary, index: number) => {
let sumVal = summary.value;
if (component.settings.type === quack.ComponentType.COMPONENT_TYPE_EDGE_TRIGGERED) {
if (
component.settings.subtype ===
quack.EdgeTriggeredSubtype.EDGE_TRIGGERED_SUBTYPE_WIND_SPEED &&
!isNaN(Number(sumVal))
) {
//make sure sumVal is a number
sumVal = Number(sumVal);
//get the period of time that all the measurements are in and convert it to seconds
let measurementInterval = Number(component.settings.measurementPeriodMs) / 1000;
//formula to determine the km/h
sumVal = (sumVal / measurementInterval) * 2.4;
//round to 2 decimal places and add the units
sumVal = Math.round(sumVal * 100) / 100 + "km/h";
}
}
return (
<span key={index}>
{summary.label + ": "} <span style={{ color: summary.colour }}>{sumVal}</span>{" "}
<br />
</span>
);
})}
</Typography>
</Grid>
{!props.omitTime && (
<Grid item xs={12}>
<Typography color="textSecondary" variant="caption">
{moment(timestamp).fromNow()}
</Typography>
</Grid>
)}
</Grid>
);
};
const getTableCellSummaryComponent = (summaries: Summary[]): any => {
if (summaries.length < 1) {
return <span>Unknown</span>;
}
const blacklist: Array<any> = [];
const filteredSummaries = summaries.filter((item: any) => !blacklist.includes(item.label));
return (
<Grid container direction="row" justify={centered ? "center" : "flex-start"} spacing={1}>
{filteredSummaries.map((summary: Summary, i: number) => {
let sumVal = summary.value;
if (component.settings.type === quack.ComponentType.COMPONENT_TYPE_EDGE_TRIGGERED) {
if (
component.settings.subtype ===
quack.EdgeTriggeredSubtype.EDGE_TRIGGERED_SUBTYPE_WIND_SPEED &&
!isNaN(Number(sumVal))
) {
//make sure sumVal is a number
sumVal = Number(sumVal);
//get the period of time that all the measurements are in and convert it to seconds
let measurementInterval = Number(component.settings.measurementPeriodMs) / 1000;
//formula to determine the km/h
sumVal = (sumVal / measurementInterval) * 2.4;
//round to 2 decimal places and add the units
sumVal = Math.round(sumVal * 100) / 100 + "km/h";
}
}
return (
<Grid item key={summary.label}>
{summary.label + ": "}
<span style={{ color: summary.colour }}>{sumVal}</span>
{i + 1 !== filteredSummaries.length && ","}
</Grid>
);
})}
</Grid>
);
};
const getDenseSummary = (summaries: Summary[]): any => {
if (summaries.length < 1) {
return null;
}
const blacklist: Array<any> = ["Temperature (Air)", "Moisture (Air)"];
const filteredSummaries = summaries.filter(
(item: any) =>
!blacklist.includes(item.label) && (item.label !== "Grain Type" || item.value !== "None")
);
let overlays: any = [];
return (
<Box width={"100%"} component="div">
{filteredSummaries.map((summary: Summary, i: number) => {
let sumVal = summary.value;
if (component.settings.type === quack.ComponentType.COMPONENT_TYPE_EDGE_TRIGGERED) {
if (
component.settings.subtype ===
quack.EdgeTriggeredSubtype.EDGE_TRIGGERED_SUBTYPE_WIND_SPEED &&
!isNaN(Number(sumVal))
) {
//make sure sumVal is a number
sumVal = Number(sumVal);
//get the period of time that all the measurements are in and convert it to seconds
let measurementInterval = Number(component.settings.measurementPeriodMs) / 1000;
//formula to determine the km/h
sumVal = (sumVal / measurementInterval) * 2.4;
//round to 2 decimal places and add the units
sumVal = Math.round(sumVal * 100) / 100 + "km/h";
}
}
if (component.settings.hasOverlays) {
let overlay: pond.ComponentOverlays = pond.ComponentOverlays.create();
let found = false;
component.settings.overlays.forEach(ov => {
if (ov.measurementType === summary.type) {
//check if the sum value is within the overlay min-max
let numVal = parseFloat(sumVal);
if (numVal >= ov.min && numVal <= ov.max) {
overlay = ov;
found = true;
}
}
});
if (found) {
overlays.push(
<React.Fragment key={"overlay" + i}>
<Grid item>
<Box
width="20px"
height="20px"
borderRadius="50%"
className={classes.on}
style={{ background: overlay.colour }}
/>
</Grid>
<Grid item>
<Typography
variant="caption"
component="span"
style={{ marginLeft: 5, marginRight: 5 }}>
{overlay.message}
</Typography>
</Grid>
</React.Fragment>
);
}
}
return (
<Typography key={summary.label} variant="caption" component="span">
<span style={{ color: summary.colour }}>{sumVal}</span>
{i + 1 !== filteredSummaries.length && ", "}
</Typography>
);
})}
{!props.omitTime && (
<Box component="span">
{" - "}
<Typography color="textSecondary" variant="caption">
{moment(timestamp).fromNow()}
</Typography>
</Box>
)}
<Grid container direction="row">
{overlays}
</Grid>
</Box>
);
};
if (summaries.length <= 0) return noSummary();
if (dense) return getDenseSummary(summaries);
if (tableCell) return getTableCellSummaryComponent(summaries);
return getSummaryComponent(summaries);
}

View file

@ -0,0 +1,104 @@
import { Box, createStyles, makeStyles, Theme } from "@material-ui/core";
import { Component } from "models";
import { getMeasurementSummary } from "pbHelpers/ComponentType";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
import { or } from "utils";
interface Props {
reading: pond.Measurement | undefined | null;
component: Component;
mode?: pond.BinMode;
index?: number;
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
"@keyframes ripple": {
from: {
transform: "scale(.8)",
opacity: 1
},
to: {
transform: "scale(1.4)",
opacity: 0
}
},
on: {
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
animationName: "$ripple",
animationDuration: "1.2s",
animationTimingFunction: "ease-in-out",
animationIterationCount: "infinite"
},
off: {
backgroundColor: "transparent"
}
})
);
//NOREF: has no references in codebase, is this not used anywhere?
export default function SensorLight(props: Props) {
const { mode, reading, component, index } = props;
const classes = useStyles();
const [color, setColor] = useState(undefined);
useEffect(() => {
if (
reading &&
reading.measurement &&
reading.measurement.id &&
reading.measurement.id.type !== null &&
reading.measurement.id.type !== undefined
) {
const grainCableFilters = () => {
return {
grainType: component.settings.grainType,
grainFilledTo: component.settings.grainFilledTo
};
};
let id = quack.ComponentID.fromObject(reading.measurement.id);
let filters = { isTableCellMode: false };
if (id.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
filters = { ...filters, ...grainCableFilters() };
}
getMeasurementSummary(
id.type,
or(component.settings.subtype, 0),
quack.Measurement.create(reading.measurement ? reading.measurement : undefined),
filters
).then(summaries => {
if (index && index < summaries.length) {
setColor(summaries[index].colour);
} else {
summaries.forEach(sum => {
setColor(sum.colour);
});
}
});
}
}, [
index,
reading,
component.settings.subtype,
component.settings.grainFilledTo,
component.settings.grainType
]);
if (mode === undefined || mode === pond.BinMode.BIN_MODE_NONE || color === undefined) {
return null;
}
return (
<Box
width="8px"
height="8px"
borderRadius="50%"
style={{ backgroundColor: color }}
className={classes.on}
/>
);
}

View file

@ -0,0 +1,37 @@
import TemperatureDarkIcon from "assets/components/temperatureDark.png";
import TemperatureLightIcon from "assets/components/temperatureLight.png";
import { ImgIcon } from "common/ImgIcon";
import { useThemeType } from "hooks";
import React from "react";
interface Props {
type?: "light" | "dark";
heightWidth?: number;
}
export default function TemperatureIcon(props: Props) {
const themeType = useThemeType();
const { type, heightWidth } = props;
const lightIcon = () => {
return TemperatureLightIcon;
};
const darkIcon = () => {
return TemperatureDarkIcon;
};
const src = () => {
if (type) {
return type === "light" ? lightIcon() : darkIcon();
}
return themeType === "light" ? darkIcon() : lightIcon();
};
if (heightWidth) {
return <img alt="tempIcon" src={src()} height={heightWidth} width={heightWidth} />;
}
return <ImgIcon alt="bins" src={src()} />;
}

View file

@ -0,0 +1,311 @@
import { Box, Grid, Typography, makeStyles, createStyles, Theme } from "@material-ui/core";
import GrainDescriber from "grain/GrainDescriber";
import { Component } from "models";
import { convertedUnitMeasurement } from "models/UnitMeasurement";
import moment from "moment";
import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import Divider from "@material-ui/core/Divider";
import { Plenum } from "models/Plenum";
import { Pressure } from "models/Pressure";
import { GrainCable } from "models/GrainCable";
import { extension, Summary } from "pbHelpers/ComponentType";
interface Props {
component: Component | Plenum | Pressure | GrainCable;
reading?: convertedUnitMeasurement | null;
tableCell?: boolean;
centered?: boolean;
dense?: boolean;
omitTime?: boolean;
onlyRecent?: boolean;
largeText?: boolean;
boldMeasurements?: boolean;
excludedNodes?: number[];
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
"@keyframes ripple": {
from: {
transform: "scale(.8)",
opacity: 1
},
to: {
transform: "scale(1.4)",
opacity: 0
}
},
on: {
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
animationName: "$ripple",
animationDuration: "1.2s",
animationTimingFunction: "ease-in-out",
animationIterationCount: "infinite"
},
off: {
backgroundColor: "transparent"
},
divider: {
margin: theme.spacing(1)
}
})
);
export default function UnitMeasurementSummary(props: Props) {
const {
component,
reading,
tableCell,
centered,
dense,
onlyRecent,
largeText,
boldMeasurements,
excludedNodes
} = props;
const [summaries, setSummaries] = useState<Summary[]>([]);
const [timestamp, setTimestamp] = useState("");
const [recent, setRecent] = useState(true);
const classes = useStyles();
useEffect(() => {
if (reading) {
if (onlyRecent) {
setRecent(
moment().diff(moment(reading.timestamp), "milliseconds") <=
component.settings.reportPeriodMs
);
}
setTimestamp(reading.timestamp);
setSummaries(
extension(component.type(), component.subType()).unitMeasurementSummary(
reading,
excludedNodes
)
);
}
}, [component, reading, excludedNodes]); // eslint-disable-line react-hooks/exhaustive-deps
const noSummary = () => {
return (
<Typography variant="body1" color="textPrimary">
No Data
</Typography>
);
};
const noRecent = () => {
return (
<Typography variant="body1" color="textPrimary">
No Recent Data
</Typography>
);
};
const getSummaryComponent = (summaries: Summary[]): any => {
if (summaries.length < 1) {
return noSummary();
}
let overlays: JSX.Element[] = [];
let grain = GrainDescriber(component.settings.grainType);
return (
<Grid container>
<Grid item xs={12}>
<Typography variant={largeText ? "body1" : "caption"} color="textPrimary">
{summaries.map((summary: Summary, index: number) => {
let sumVal = summary.value;
if (component.settings.hasOverlays) {
let overlay: pond.ComponentOverlays = pond.ComponentOverlays.create();
let found = false;
component.settings.overlays.forEach(ov => {
if (ov.measurementType === summary.type) {
//check if the sum value is within the overlay min-max, currently only checks based off the first node
//TODO-CS: possibly consider having more overlays by checking all nodes
let numVal = parseFloat(sumVal);
if (numVal >= ov.min && numVal <= ov.max) {
overlay = ov;
found = true;
}
}
});
if (found) {
overlays.push(
<React.Fragment key={"overlay" + index}>
<Grid item>
<Box
width="20px"
height="20px"
borderRadius="50%"
className={classes.on}
style={{ background: overlay.colour }}
/>
</Grid>
<Grid item>
<Typography
variant={largeText ? "body1" : "caption"}
component="span"
style={{ marginLeft: 5, marginRight: 5 }}>
{overlay.message}
</Typography>
</Grid>
</React.Fragment>
);
}
}
return (
<span key={index}>
{summary.label}
{summary.error && <span style={{ color: "red" }}>{" [error(s)]"}</span>}
{": "}
<span style={{ color: summary.colour, fontWeight: boldMeasurements ? 650 : 500 }}>
{sumVal}
</span>{" "}
<br />
</span>
);
})}
</Typography>
</Grid>
{component.settings.grainType !== pond.Grain.GRAIN_NONE &&
component.settings.grainType !== pond.Grain.GRAIN_INVALID && (
<Typography variant={largeText ? "body1" : "caption"} color="textPrimary">
<span key={"grainType"}>
Grain Type: <span style={{ color: grain.colour }}>{grain.name}</span>
</span>
</Typography>
)}
{!props.omitTime && (
<Grid item xs={12}>
<Typography color="textSecondary" variant={largeText ? "body1" : "caption"}>
{moment(timestamp).fromNow()}
</Typography>
</Grid>
)}
{overlays.length > 0 && (
<React.Fragment>
<Grid item xs={12}>
<Divider variant="middle" className={classes.divider} />
</Grid>
<Grid container direction="row">
{overlays}
</Grid>
</React.Fragment>
)}
</Grid>
);
};
const getTableCellSummaryComponent = (summaries: Summary[]): any => {
if (summaries.length < 1 || summaries === undefined) {
return <span>Unknown</span>;
}
const blacklist: Array<any> = [];
const filteredSummaries = summaries.filter((item: any) => !blacklist.includes(item.label));
return (
<Grid container direction="row" justify={centered ? "center" : "flex-start"} spacing={1}>
{filteredSummaries.map((summary: Summary, i: number) => {
let sumVal = summary.value;
return (
<Grid item key={"summary-" + i}>
{summary.label}
{summary.error && <span style={{ color: "red" }}>{" [error(s)]"}</span>}
{": "}
<span style={{ color: summary.colour, fontWeight: boldMeasurements ? 650 : 500 }}>
{sumVal}
</span>{" "}
{i + 1 !== filteredSummaries.length && ","}
</Grid>
);
})}
</Grid>
);
};
const getDenseSummary = (summaries: Summary[]): any => {
if (summaries.length < 1) {
return null;
}
const blacklist: Array<any> = ["Temperature (Air)", "Moisture (Air)"];
const filteredSummaries = summaries.filter(
(item: any) =>
!blacklist.includes(item.label) && (item.label !== "Grain Type" || item.value !== "None")
);
let overlays: any = [];
return (
<Box width={"100%"} component="div" key={Math.random()}>
{filteredSummaries.map((summary: Summary, i: number) => {
let sumVal = summary.value;
if (component.settings.hasOverlays) {
let overlay: pond.ComponentOverlays = pond.ComponentOverlays.create();
let found = false;
component.settings.overlays.forEach(ov => {
if (ov.measurementType === summary.type) {
//check if the sum value is within the overlay min-max
let numVal = parseFloat(sumVal);
if (numVal >= ov.min && numVal <= ov.max) {
overlay = ov;
found = true;
}
}
});
if (found) {
overlays.push(
<React.Fragment key={"overlay" + i}>
<Grid item>
<Box
width="20px"
height="20px"
borderRadius="50%"
className={classes.on}
style={{ background: overlay.colour }}
/>
</Grid>
<Grid item>
<Typography
variant={largeText ? "body1" : "caption"}
component="span"
style={{ marginLeft: 5, marginRight: 5 }}>
{overlay.message}
</Typography>
</Grid>
</React.Fragment>
);
}
}
return (
<Typography
key={"summary-" + i}
variant={largeText ? "body1" : "caption"}
component="span">
<span style={{ color: summary.colour }}>{sumVal}</span>
{i + 1 !== filteredSummaries.length && ", "}
</Typography>
);
})}
{!props.omitTime && (
<Box component="span">
{" - "}
<Typography color="textSecondary" variant={largeText ? "body1" : "caption"}>
{moment(timestamp).fromNow()}
</Typography>
</Box>
)}
<Grid container direction="row">
{overlays}
</Grid>
</Box>
);
};
if (!recent) return noRecent();
if (summaries.length <= 0) return noSummary();
if (dense) return getDenseSummary(summaries);
if (tableCell) return getTableCellSummaryComponent(summaries);
return getSummaryComponent(summaries);
}