frontend/src/component/ComponentForm.tsx

1316 lines
44 KiB
TypeScript

import {
Accordion,
AccordionDetails,
AccordionSummary,
Alert,
AlertTitle,
Button,
Collapse,
FormControl,
FormControlLabel,
Grid2 as Grid,
IconButton,
InputAdornment,
MenuItem,
Switch,
TextField,
Theme,
Tooltip,
Typography
} from "@mui/material";
import {
ExpandMore,
Close as CloseIcon
} from "@mui/icons-material";
import PeriodSelect from "common/time/PeriodSelect";
import SearchSelect, { Option } from "common/SearchSelect";
import { cloneDeep } from "lodash";
import { Component, Device } from "models";
import {
extension,
getMeasurements,
hasDimensions,
hasFan,
isController,
isMultiSensor,
primaryMeasurement
} from "pbHelpers/ComponentType";
import { describeMeasurement, MeasurementDescriber } from "pbHelpers/MeasurementDescriber";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useEffect, useState } from "react";
import { bestUnit, milliToX } from "common/time/duration";
import { useGlobalState } from "providers";
import { green, red } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { getDistanceUnit } from "utils";
import { GrainOptions } from "grain";
import CompModes from "component/ComponentMode.json";
import FanPicker from "fans/fanPicker";
const useStyles = makeStyles((theme: Theme) => {
return ({
stepper: {
paddingRight: 0,
paddingLeft: 0
},
switchControl: {
marginLeft: theme.spacing(0.25),
[theme.breakpoints.up("sm")]: {
marginLeft: theme.spacing(1)
}
},
selectInput: {
minHeight: "48px"
},
fullWidth: {
width: "100%"
},
heading: {
fontSize: theme.typography.pxToRem(13),
flexBasis: "25%",
flexShrink: 0
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(13),
color: theme.palette.text.secondary
},
greenButton: {
color: green["600"]
},
redButton: {
color: red["600"]
},
noPadding: {
padding: 0
},
dataUsageCard: { width: "100%" },
dataUsageHeader: { paddingBottom: 0 },
dataUsageIcon: { color: "var(--status-warning)" },
dataUsageContent: { paddingTop: 0 }
})
});
interface Props {
device: Device;
component: Component;
componentChanged: (component: Component, formValid: boolean) => void;
canEdit: boolean;
}
interface ComponentData {
component: Component;
measure: boolean;
report: boolean;
partiallyFilled: boolean;
coefficient: string;
offset: string;
//these are for the new calibrations
coefficients: string[];
offsets: string[];
length: string;
height: string;
width: string;
sensorDistance: string;
}
export default function ComponentForm(props: Props) {
const classes = useStyles();
const grainOptions = GrainOptions();
const [{ user }] = useGlobalState();
const { device, component, componentChanged, canEdit } = props;
const [reportExpanded, setReportExpanded] = useState(false);
const [dataUsageWarningDismissed, setDataUsageWarningDismissed] = useState(true);
const [form, setForm] = useState<ComponentData>({
component: cloneDeep(component),
measure: false,
report: false,
partiallyFilled: false,
coefficient: "1",
offset: "0",
coefficients: [""],
offsets: [""],
length: "0",
height: "0",
width: "0",
sensorDistance: "0"
});
const [compMode, setCompMode] = useState<any>();
//const [numCalibrations, setNumCalibrations] = useState(0)
useEffect(() => {
let formComponent = cloneDeep(component);
let describer = describeMeasurement(
primaryMeasurement(formComponent.settings.type),
formComponent.settings.type,
formComponent.settings.subtype
);
let offset = describer.toDisplay(formComponent.settings.calibrationOffset);
let formCoefficients: string[] = [];
let formOffsets: string[] = [];
//setNumCalibrations(formComponent.settings.calibrations.length)
formComponent.settings.calibrations.forEach(calibration => {
formCoefficients.push(calibration.calibrationCoefficient.toString());
formOffsets.push(describer.toDisplay(calibration.calibrationOffset).toString());
});
if (
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE &&
formComponent.settings.grainType === pond.Grain.GRAIN_INVALID
) {
formComponent.settings.grainType = pond.Grain.GRAIN_NONE;
}
setReportExpanded(
formComponent.settings.measurementPeriodMs !== formComponent.settings.reportPeriodMs
);
setDataUsageWarningDismissed(localStorage.getItem("dataUsageWarningDismissed") !== null);
CompModes.forEach((mode: any) => {
if (formComponent.type() === mode.type && formComponent.subType() === mode.subtype) {
setCompMode(mode);
}
});
//get the dimension values for the form and convert them to the form display values (m or ft)
let length = formComponent.settings.containerDimensions?.lengthCm ?? 0;
let width = formComponent.settings.containerDimensions?.widthCm ?? 0;
let height = formComponent.settings.containerDimensions?.heightCm ?? 0;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
length = length / 100;
width = width / 100;
height = height / 100;
} else if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
length = length / 30.48;
width = width / 30.48;
height = height / 30.48;
}
let sensorDistance = formComponent.settings.sensorDistanceCm ?? 0;
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
sensorDistance = sensorDistance / 100;
} else if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
sensorDistance = sensorDistance / 30.48;
}
setForm({
component: formComponent,
measure: formComponent.settings.measurementPeriodMs > 0,
report: formComponent.settings.reportPeriodMs > 0,
partiallyFilled: formComponent.settings.grainFilledTo > 0,
coefficient: formComponent.settings.calibrationCoefficient.toString(),
offset: offset.toString(),
coefficients: formCoefficients,
offsets: formOffsets,
length: length.toFixed(2),
width: width.toFixed(2),
height: height.toFixed(2),
sensorDistance: sensorDistance.toFixed(2)
});
}, [component]);
const tidyComponent = (component: Component): Component => {
const { measure, report } = form;
if (!report) {
component.settings.reportPeriodMs = 0;
}
if (!measure) {
component.settings.measurementPeriodMs = 0;
}
return component;
};
const isFormValid = () => {
let { component } = form;
return (
isNameValid(component.settings.name) &&
isMeasurementPeriodValid() &&
isReportPeriodValid() &&
isFilledToValid() &&
isCoefficientValid() &&
isOffsetValid() &&
isMinCycleTimeValid() &&
isLengthDimensionValid() &&
isHeightDimensionValid() &&
isWidthDimensionValid() &&
isSensorDistanceValid()
);
};
useEffect(() => {
componentChanged(form.component, isFormValid());
}, [form.component, componentChanged]); // eslint-disable-line react-hooks/exhaustive-deps
const isNameValid = (name: string): boolean => {
return name.length > 0;
};
const isMeasurementPeriodValid = () => {
const { measure, component } = form;
const ext = extension(component.settings.type, component.settings.subtype);
let valid = true;
if (
measure &&
(component.settings.measurementPeriodMs < 1000 ||
component.settings.measurementPeriodMs < ext.minMeasurementPeriodMs)
) {
valid = false;
}
return valid;
};
const isFilledToValid = () => {
const { partiallyFilled, component } = form;
return (partiallyFilled && component.settings.grainFilledTo > 0) || !partiallyFilled;
};
const minMeasurementPeriodDescription = () => {
const { component } = form;
const ext = extension(component.settings.type, component.settings.subtype);
const min = Math.max(1000, ext.minMeasurementPeriodMs);
const unit = bestUnit(min);
const value = milliToX(min, unit).toString();
return value + " " + unit;
};
const isReportPeriodValid = () => {
const { report, component } = form;
const ext = extension(component.settings.type, component.settings.subtype);
let valid = true;
if (
report &&
(component.settings.reportPeriodMs < 10000 ||
component.settings.reportPeriodMs < ext.minMeasurementPeriodMs)
) {
valid = false;
}
return valid;
};
const minReportPeriodDescription = () => {
const { component } = form;
const ext = extension(component.settings.type, component.settings.subtype);
const min = Math.max(10000, ext.minMeasurementPeriodMs);
const unit = bestUnit(min);
const value = milliToX(min, unit).toString();
return value + " " + unit;
};
const isMinCycleTimeValid = () => {
const { component } = form;
const ext = extension(component.settings.type, component.settings.subtype);
if (!ext.isController) return true;
let min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0);
return component.settings.minCycleTimeMs >= min;
};
const minCycleTimeDescription = () => {
const { component } = form;
const ext = extension(component.settings.type, component.settings.subtype);
if (!ext.isController) return "";
const min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0);
const unit = bestUnit(min);
const value = milliToX(min, unit).toString();
return "Must be at least " + value + " " + unit;
};
const handleNameChanged = (event: any) => {
let f = cloneDeep(form);
f.component.settings.name = event.target.value;
setForm(f);
};
const handleOutputModeChanged = (event: any) => {
let f = cloneDeep(form);
f.component.settings.defaultOutputState = event.target.value;
setForm(f);
};
const handleReportPeriodChanged = (ms: number) => {
let f = cloneDeep(form);
f.component.settings.reportPeriodMs = ms;
setForm(f);
};
const handleMeasurementPeriodChanged = (ms: number) => {
let f = cloneDeep(form);
f.component.settings.measurementPeriodMs = ms;
if (!reportExpanded) {
f.component.settings.reportPeriodMs = ms;
}
setForm(f);
};
const handleMinCycleTimeChanged = (ms: number) => {
let f = cloneDeep(form);
f.component.settings.minCycleTimeMs = Number(ms);
setForm(f);
};
const toggleMeasure = (event: any) => {
let f = cloneDeep(form);
f.measure = event.target.checked;
if (!reportExpanded) {
f.report = event.target.checked;
}
setForm(f);
};
const toggleReport = (event: any) => {
let f = cloneDeep(form);
f.report = event.target.checked;
setForm(f);
};
const togglePartiallyFilled = (event: any) => {
let f = cloneDeep(form);
f.partiallyFilled = event.target.checked;
if (event.target.checked && f.component.settings.grainFilledTo <= 0) {
f.component.settings.grainFilledTo = 1;
}
if (!event.target.checked) {
f.component.settings.grainFilledTo = 0;
}
setForm(f);
};
const updateGrainType = (option: Option | null) => {
let f = cloneDeep(form);
if (!option) {
f.component.settings.grainType = pond.Grain.GRAIN_INVALID;
} else {
let grainType = pond.Grain[option.value as keyof typeof pond.Grain];
f.component.settings.grainType = grainType;
if (grainType === pond.Grain.GRAIN_NONE) {
f.component.settings.defaultMutations.splice(
f.component.settings.defaultMutations.indexOf(pond.Mutator.MUTATOR_EMC, 1)
);
} else if (!f.component.settings.defaultMutations.includes(pond.Mutator.MUTATOR_EMC)) {
f.component.settings.defaultMutations.push(pond.Mutator.MUTATOR_EMC);
}
}
setForm(f);
};
const updateGrainFilledTo = (event: any) => {
let f = cloneDeep(form);
f.component.settings.grainFilledTo = Number(event.target.value);
setForm(f);
};
const toggleCalibrate = () => {
let f = cloneDeep(form);
f.component.settings.calibrate = !f.component.settings.calibrate;
let isZero = f.coefficient === "0" || f.coefficient === "";
if (f.component.settings.calibrate && isZero) {
f.coefficient = "1";
f.component.settings.calibrationCoefficient = 1;
}
let offsetZero = f.offset === "0" || f.offset === "";
if (f.component.settings.calibrate && offsetZero) {
f.offset = "1";
f.component.settings.calibrationOffset = 1;
}
setForm(f);
};
const isCoefficientValid = () => {
let calibrate = form.component.settings.calibrate;
let coefficient = Number(form.coefficient);
let valid = !isNaN(coefficient) && (!calibrate || (calibrate && coefficient > 0));
return valid;
};
const changeCoefficient = (event: any) => {
let f = cloneDeep(form);
let value = event.target.value;
f.coefficient = value;
if (!isNaN(Number(value))) {
f.component.settings.calibrationCoefficient = +Number(value);
}
setForm(f);
};
const updateCoefficients = (event: any, index: number) => {
let f = cloneDeep(form);
let value = event.target.value;
f.coefficients[index] = value;
if (!isNaN(Number(value))) {
f.component.settings.calibrations[index].calibrationCoefficient = +Number(value);
}
setForm(f);
};
const isOffsetValid = (min?: number, max?: number) => {
let minimum = min ?? 0;
let maximum = max ?? 100;
if (compMode && form.component.settings.calibrate) {
return Number(form.offset) <= maximum && Number(form.offset) > minimum;
}
return !isNaN(Number(form.offset));
};
const changeOffset = (event: any) => {
let f = cloneDeep(form);
f.offset = event.target.value;
if (!isNaN(Number(event.target.value))) {
f.component.settings.calibrationOffset = describeMeasurement(
primaryMeasurement(f.component.settings.type),
f.component.settings.type,
f.component.settings.subtype
).toStored(+event.target.value);
}
setForm(f);
};
const updateOffsets = (event: any, index: number) => {
let f = cloneDeep(form);
f.offsets[index] = event.target.value;
if (!isNaN(+event.target.value)) {
f.component.settings.calibrations[index].calibrationOffset = describeMeasurement(
primaryMeasurement(f.component.settings.type),
f.component.settings.type,
f.component.settings.subtype
).toStored(+event.target.value);
}
setForm(f);
};
const updateCalibrationTypes = (event: any, index: number) => {
let f = cloneDeep(form);
f.component.settings.calibrations[index].type = event.target.value as quack.MeasurementType;
setForm(f);
};
const changeAverages = (event: any) => {
let f = cloneDeep(form);
if (!isNaN(Number(event.target.value))) {
f.component.settings.smoothingAverages = event.target.value;
}
setForm(f);
};
const changeLengthDimension = (event: any) => {
let f = cloneDeep(form);
f.length = event.target.value;
if (!isNaN(Number(event.target.value))) {
let dimensions = f.component.settings.containerDimensions;
let value = Number(event.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
value = value * 100;
} else if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
value = value * 30.48;
}
if (dimensions !== undefined && dimensions !== null) {
dimensions.lengthCm = value;
} else {
dimensions = pond.Dimensions.create({
heightCm: 0,
lengthCm: value,
widthCm: 0
});
}
f.component.settings.containerDimensions = dimensions;
}
setForm(f);
};
const isLengthDimensionValid = () => {
return !isNaN(Number(form.length));
};
const changeHeightDimension = (event: any) => {
let f = cloneDeep(form);
f.height = event.target.value;
if (!isNaN(Number(event.target.value))) {
let dimensions = f.component.settings.containerDimensions;
let value = Number(event.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
value = value * 100;
} else if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
value = value * 30.48;
}
if (dimensions !== undefined && dimensions !== null) {
dimensions.heightCm = value;
} else {
dimensions = pond.Dimensions.create({
heightCm: value,
lengthCm: 0,
widthCm: 0
});
}
f.component.settings.containerDimensions = dimensions;
}
setForm(f);
};
const isHeightDimensionValid = () => {
return !isNaN(Number(form.height));
};
const changeWidthDimension = (event: any) => {
let f = cloneDeep(form);
f.width = event.target.value;
if (!isNaN(Number(event.target.value))) {
let dimensions = f.component.settings.containerDimensions;
let value = Number(event.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
value = value * 100;
} else if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
value = value * 30.48;
}
if (dimensions !== undefined && dimensions !== null) {
dimensions.widthCm = value;
} else {
dimensions = pond.Dimensions.create({
heightCm: 0,
lengthCm: 0,
widthCm: value
});
}
f.component.settings.containerDimensions = dimensions;
}
setForm(f);
};
const isWidthDimensionValid = () => {
return !isNaN(Number(form.width));
};
const changeSensorDistance = (event: any) => {
let f = cloneDeep(form);
f.sensorDistance = event.target.value;
if (!isNaN(Number(event.target.value))) {
let value = Number(event.target.value);
if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
value = value * 100;
} else if (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
value = value * 30.48;
}
f.component.settings.sensorDistanceCm = value;
}
setForm(f);
};
const isSensorDistanceValid = () => {
return !isNaN(Number(form.sensorDistance));
};
const selectText = (event: any) => {
event.target.select();
};
const findSelectedGrain = (options: Option[]): Option | undefined => {
const { component } = form;
return options.find(o => {
let target = pond.Grain[o.value as keyof typeof pond.Grain];
return component.settings.grainType === target;
});
};
const grainSelect = () => {
let selected = findSelectedGrain(grainOptions);
return (
<SearchSelect
selected={selected}
changeSelection={updateGrainType}
label="Grain Type"
options={grainOptions}
group
/>
);
};
const dismissDataUsageWarning = () => {
localStorage.setItem("dataUsageWarningDismissed", "true");
setDataUsageWarningDismissed(true);
};
const blanketCalibration = () => {
const { component, coefficient, offset } = form;
return (
<React.Fragment>
<Grid size={{ xs: 4, sm: 3 }} >
<Tooltip
title="Calibrates the primary measurement by multiplying by the coefficient and adding the offset"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="calibrate"
aria-label="calibrate"
color="secondary"
/>
}
label="Calibrate"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 8, sm: 4 }}>
<TextField
id="coefficient"
label="Coefficient"
disabled={!component.settings.calibrate}
error={!isCoefficientValid()}
value={coefficient}
onChange={changeCoefficient}
onFocus={selectText}
margin="normal"
fullWidth
type="text"
variant="outlined"></TextField>
</Grid>
<Grid size={{ xs: 12, sm: 5 }}>
<TextField
id="offset"
label="Offset"
disabled={!component.settings.calibrate}
error={!isOffsetValid}
value={offset}
onChange={changeOffset}
onFocus={selectText}
margin="normal"
fullWidth
type="text"
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{describeMeasurement(
primaryMeasurement(component.settings.type),
component.settings.type,
component.settings.subtype
).unit()}
</InputAdornment>
)
}}></TextField>
</Grid>
</React.Fragment>
);
};
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
const { component } = form;
return describeMeasurement(measurementType, component.type(), component.subType());
};
const availableMeasurementTypes = (type: quack.ComponentType): quack.MeasurementType[] => {
return getMeasurements(type).map(m => m.measurementType);
};
const measurementTypeMenuItems = () => {
return availableMeasurementTypes(form.component.type()).map(measurementType => (
<MenuItem key={measurementType} value={measurementType}>
{describeSource(measurementType).label()}
</MenuItem>
));
};
const individualCalibration = () => {
const { component, coefficients, offsets } = form;
return (
<React.Fragment>
<Grid size={{ xs: 4, sm: 3 }}>
<Tooltip
title="Calibrates the primary measurement by multiplying by the coefficient and adding the offset"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="calibrate"
aria-label="calibrate"
color="secondary"
/>
}
label="Calibrate"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
{component.settings.calibrations.map((calibration, i) => {
return (
<React.Fragment key={"calibration" + i}>
{i > 0 && <Grid size={{ xs: 4, sm: 3 }}></Grid>}
<Grid size={{ xs: i > 0 ? 12 : 8, sm: 3 }} >
<TextField
id="coefficient"
label="Coefficient"
disabled={!component.settings.calibrate}
//error={!isCoefficientValid()}
value={coefficients[i] ?? "0"}
onChange={event => {
updateCoefficients(event, i);
}}
onFocus={selectText}
margin="normal"
fullWidth
type="text"
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
id="offset"
label="Offset"
disabled={!component.settings.calibrate}
//error={!isOffsetValid}
value={offsets[i] ?? 0}
onChange={event => {
updateOffsets(event, i);
}}
onFocus={selectText}
margin="normal"
fullWidth
type="text"
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 12, sm: 3 }}>
<TextField
select
id="measurementType"
label="Measurement"
// error={
// !isMeasurementTypeValid(interaction.settings.conditions[index].measurementType)
// }
disabled={!component.settings.calibrate}
value={calibration.type ?? quack.MeasurementType.MEASUREMENT_TYPE_INVALID}
onChange={event => updateCalibrationTypes(event, i)}
autoFocus={false}
margin="normal"
variant="outlined"
fullWidth
InputLabelProps={{ shrink: true }}>
<MenuItem
key={quack.MeasurementType.MEASUREMENT_TYPE_INVALID}
value={quack.MeasurementType.MEASUREMENT_TYPE_INVALID}>
All
</MenuItem>
{measurementTypeMenuItems()}
</TextField>
</Grid>
</React.Fragment>
);
})}
<Button
variant="contained"
color="primary"
onClick={() => {
let f = cloneDeep(form);
f.coefficients.push("0");
f.offsets.push("0");
f.component.settings.calibrations.push(
pond.Calibration.create({
type: quack.MeasurementType.MEASUREMENT_TYPE_INVALID,
calibrationOffset: 0,
calibrationCoefficient: 0
})
);
setForm(f);
}}>
Add Calibration
</Button>
</React.Fragment>
);
};
const generalForm = () => {
const {
component,
measure,
report,
partiallyFilled,
coefficient,
offset,
length,
height,
width,
sensorDistance
} = form;
let reportDesc = "";
let reportPeriod = component.settings.reportPeriodMs;
let measurementPeriod = component.settings.measurementPeriodMs;
if (reportPeriod === measurementPeriod) {
reportDesc = "Every measurement";
} else {
reportDesc = "Independently";
}
if (!form.report) {
reportDesc = "Not reporting";
}
let showDataUsageWarning =
!dataUsageWarningDismissed &&
form.report &&
component.settings.reportPeriodMs < 300000 &&
device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON;
return (
<React.Fragment>
<TextField
id="name"
name="name"
label="Name"
value={component.settings.name}
onChange={handleNameChanged}
margin="normal"
variant="outlined"
error={!isNameValid(component.settings.name)}
helperText={isNameValid(component.settings.name) ? "" : "Name is required"}
type="text"
fullWidth
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
/>
{/* TODO-CS: Change this to have a parameter in the extensions that gets checked rather then checking for the component type */}
{hasFan(component.type()) && (
<FanPicker
fanID={component.settings.fanId}
updateFan={(fanID: number) => {
component.settings.fanId = fanID;
if (
fanID > 0 &&
!component.settings.defaultMutations.includes(pond.Mutator.MUTATOR_CFM)
) {
component.settings.defaultMutations.push(pond.Mutator.MUTATOR_CFM);
} else if (fanID === 0 || !fanID) {
component.settings.defaultMutations.splice(
component.settings.defaultMutations.indexOf(pond.Mutator.MUTATOR_CFM)
);
}
}}
/>
)}
{component.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE &&
grainSelect()}
{component.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE && (
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
alignContent="center"
spacing={1}>
<Grid container size={{ xs: 4, sm: 3 }} justifyContent="center">
<FormControlLabel
control={
<Switch
checked={partiallyFilled}
onChange={togglePartiallyFilled}
name="partiallyFilled"
aria-label="partiallyFilled"
color="secondary"
/>
}
label={<Typography variant="caption">Partially Filled</Typography>}
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Grid>
<Grid size={{ xs: 8, sm: 9 }}>
<FormControl fullWidth>
<TextField
id="bin-filled-to"
label="Filled Up To Node"
value={form.component.settings.grainFilledTo}
onChange={updateGrainFilledTo}
disabled={!partiallyFilled}
error={!isFilledToValid()}
helperText={!isFilledToValid() ? "Must be at least 1" : ""}
margin="normal"
fullWidth
variant="outlined"></TextField>
</FormControl>
</Grid>
</Grid>
)}
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
alignContent="center"
spacing={1}>
{
<Grid container size={{ xs: 4, sm: 3 }} justifyContent="center">
<FormControlLabel
control={
<Switch
checked={measure}
onChange={toggleMeasure}
name="measure"
aria-label="measure"
color="secondary"
/>
}
label={<Typography variant="caption">Measure</Typography>}
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Grid>
}
{
<Grid size={{ xs: 8, sm: 9 }}>
<FormControl fullWidth>
<PeriodSelect
id="measurementPeriodMs"
label="Period"
units={["seconds", "minutes", "hours"]}
isDisabled={!measure || !canEdit}
isError={!isMeasurementPeriodValid()}
helperText={
!isMeasurementPeriodValid()
? "Must be at least " + minMeasurementPeriodDescription()
: "How often the component is measured"
}
initialMs={component.settings.measurementPeriodMs}
onChange={handleMeasurementPeriodChanged}
/>
</FormControl>
</Grid>
}
{
<Accordion
className={classes.fullWidth}
expanded={reportExpanded}
onChange={(_, expanded) => setReportExpanded(expanded)}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography className={classes.heading}>Report</Typography>
<Typography className={classes.secondaryHeading}>{reportDesc}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
alignContent="center"
spacing={1}>
<Grid size={{ xs: 4, sm: 3 }}>
<Tooltip title="Reporting measurements uses data" placement="bottom">
<FormControlLabel
control={
<Switch
checked={report}
onChange={toggleReport}
name="report"
aria-label="report"
color="secondary"
disabled={!measure}
/>
}
label={<Typography variant="caption">Enabled</Typography>}
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 8, sm: 9 }}>
<FormControl fullWidth>
<PeriodSelect
id="reportPeriodMs"
label="Period"
units={["seconds", "minutes", "hours"]}
isDisabled={!measure || !report || !canEdit}
isError={!isReportPeriodValid()}
helperText={
!isReportPeriodValid()
? "Must be at least " + minReportPeriodDescription()
: "How often the component reports to the cloud"
}
initialMs={
component.settings.reportPeriodMs
? component.settings.reportPeriodMs
: undefined
}
onChange={handleReportPeriodChanged}
/>
</FormControl>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
}
{!compMode ? (
device.featureSupported("individualCalibrations") ? (
individualCalibration()
) : (
blanketCalibration()
)
) : (
<React.Fragment>
<Grid size={{ xs: 4, sm: 3 }}>
<Tooltip
title="Switch to be able to customize the mode of the component"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="mode"
aria-label="mode"
color="secondary"
/>
}
label="Mode"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 10, sm: 4 }}>
{compMode.mode.entryA && (
<TextField
id={compMode.mode.labelA}
label={compMode.mode.labelA}
disabled={!component.settings.calibrate}
error={!isCoefficientValid()}
value={coefficient}
onChange={changeCoefficient}
margin="normal"
fullWidth
select={compMode.mode.entryA === "select"}
type={compMode.mode.entryA}
variant="outlined">
{compMode.mode.entryA === "select" &&
compMode.mode.dictionaryA.map((elem: any) => {
if (!elem.restricted || (elem.restricted && user.hasAdmin())) {
return (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
);
}
return undefined; //TODO-CS: Time permitting re-think the structure of this function so that I don't have to return undefined
})}
</TextField>
)}
</Grid>
<Grid size={{ xs: 10, sm: 5 }}>
{compMode.mode.entryB && (
<TextField
id={compMode.mode.labelB}
label={compMode.mode.labelB}
disabled={!component.settings.calibrate}
error={!isOffsetValid(compMode.mode.minB, compMode.mode.maxB)}
value={offset}
onChange={changeOffset}
onFocus={selectText}
margin="normal"
fullWidth
type={compMode.mode.entryB}
variant="outlined">
{compMode.mode.entryB === "select" &&
compMode.mode.dictionaryB.map((elem: any) => (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
))}
</TextField>
)}
</Grid>
</React.Fragment>
)}
<TextField
id="dataAveraging"
name="dataAveraging"
label="Data Averaging"
value={component.settings.smoothingAverages}
onChange={changeAverages}
margin="normal"
variant="outlined"
helperText={"The number of points to average together"}
type="number"
fullWidth
InputLabelProps={{ shrink: true }}
disabled={!canEdit}
/>
{hasDimensions(component.settings.type) && (
<React.Fragment>
<Typography>Dimensions</Typography>
<Grid container direction="row" spacing={1}>
<Grid size={{ xs: 12, sm: 4 }}>
<TextField
id="dimension-length"
name="dimensionLength"
//type="number"
label={"Length"}
variant="outlined"
fullWidth
margin="normal"
value={length}
onChange={changeLengthDimension}
error={!isLengthDimensionValid()}
disabled={!canEdit}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS
? "m"
: "ft"}
</InputAdornment>
)
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<TextField
id="dimension-height"
name="dimensionHeight"
//type="number"
label={"Height"}
variant="outlined"
fullWidth
margin="normal"
value={height}
onChange={changeHeightDimension}
error={!isHeightDimensionValid()}
disabled={!canEdit}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS
? "m"
: "ft"}
</InputAdornment>
)
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 4 }}>
<TextField
id="dimension-width"
name="dimensionWidth"
//type="number"
label={"Width"}
variant="outlined"
fullWidth
margin="normal"
value={width}
onChange={changeWidthDimension}
error={!isWidthDimensionValid()}
disabled={!canEdit}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS
? "m"
: "ft"}
</InputAdornment>
)
}}
/>
</Grid>
</Grid>
</React.Fragment>
)}
{isMultiSensor(component.settings.type) && (
<TextField
id="sensor-distance"
name="sensorDistance"
//type="number"
label={"Sensor Distance"}
variant="outlined"
fullWidth
helperText="The direct distance between the sensors"
margin="normal"
value={sensorDistance}
onChange={changeSensorDistance}
error={!isSensorDistanceValid()}
disabled={!canEdit}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS ? "m" : "ft"}
</InputAdornment>
)
}}
/>
)}
{isController(component.settings.type) && (
<React.Fragment>
<Grid size={{ xs: 12 }}>
<TextField
id="output-mode"
name="outputMode"
select
label={"Output Mode"}
value={component.settings.defaultOutputState}
onChange={handleOutputModeChanged}
margin="normal"
fullWidth
variant="outlined"
disabled={!canEdit}>
<MenuItem value={0}>Auto</MenuItem>
<MenuItem value={1}>On</MenuItem>
<MenuItem value={2}>Off</MenuItem>
</TextField>
</Grid>
<Grid size={{ xs: 12 }}>
<FormControl fullWidth>
<PeriodSelect
id="minCycleTimeMs"
label="Minimum cycle time"
units={["seconds", "minutes"]}
isDisabled={!canEdit}
isError={!isMinCycleTimeValid()}
initialMs={
component.settings.minCycleTimeMs
? component.settings.minCycleTimeMs
: undefined
}
onChange={handleMinCycleTimeChanged}
helperText={
isMinCycleTimeValid()
? "The minimum amount of time that the component spends in a state"
: minCycleTimeDescription()
}
/>
</FormControl>
</Grid>
</React.Fragment>
)}
{showDataUsageWarning && (
<Grid size={{ xs: 12 }}>
<Collapse in={showDataUsageWarning}>
<Alert
severity="warning"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={dismissDataUsageWarning}>
<CloseIcon fontSize="inherit" />
</IconButton>
}>
<AlertTitle>High Data Usage</AlertTitle>A report period less than 5 minutes will
use a large amount of cellular data
</Alert>
</Collapse>
</Grid>
)}
</Grid>
</React.Fragment>
);
};
return <React.Fragment>{generalForm()}</React.Fragment>;
}