added tons of files for component functionality; manual component adding added temporarily
This commit is contained in:
parent
58830d480e
commit
45ff49bcea
121 changed files with 25883 additions and 65 deletions
98
src/component/AddComponentManualDialog.tsx
Normal file
98
src/component/AddComponentManualDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
360
src/component/AddCompsFromDiag.tsx
Normal file
360
src/component/AddCompsFromDiag.tsx
Normal 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>;
|
||||
}
|
||||
213
src/component/ComponentActions.tsx
Normal file
213
src/component/ComponentActions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
390
src/component/ComponentCard.tsx
Normal file
390
src/component/ComponentCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
824
src/component/ComponentChart.tsx
Normal file
824
src/component/ComponentChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
266
src/component/ComponentDiagnostics.tsx
Normal file
266
src/component/ComponentDiagnostics.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1322
src/component/ComponentForm.tsx
Normal file
1322
src/component/ComponentForm.tsx
Normal file
File diff suppressed because it is too large
Load diff
69
src/component/ComponentHistory.tsx
Normal file
69
src/component/ComponentHistory.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
139
src/component/ComponentMode.json
Normal file
139
src/component/ComponentMode.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
184
src/component/ComponentOrder.tsx
Normal file
184
src/component/ComponentOrder.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1055
src/component/ComponentSettings.tsx
Normal file
1055
src/component/ComponentSettings.tsx
Normal file
File diff suppressed because it is too large
Load diff
61
src/component/ComponentTypeChip.tsx
Normal file
61
src/component/ComponentTypeChip.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
414
src/component/ExportDataSettings.tsx
Normal file
414
src/component/ExportDataSettings.tsx
Normal 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
116
src/component/GPS.tsx
Normal 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);
|
||||
38
src/component/HumidityIcon.tsx
Normal file
38
src/component/HumidityIcon.tsx
Normal 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()} />;
|
||||
}
|
||||
278
src/component/MeasurementSummary.tsx
Normal file
278
src/component/MeasurementSummary.tsx
Normal 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);
|
||||
}
|
||||
104
src/component/SensorLight.tsx
Normal file
104
src/component/SensorLight.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
37
src/component/TemperatureIcon.tsx
Normal file
37
src/component/TemperatureIcon.tsx
Normal 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()} />;
|
||||
}
|
||||
311
src/component/UnitMeasurementSummary.tsx
Normal file
311
src/component/UnitMeasurementSummary.tsx
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue