1413 lines
47 KiB
TypeScript
1413 lines
47 KiB
TypeScript
import {
|
|
Accordion,
|
|
AccordionDetails,
|
|
AccordionSummary,
|
|
Alert,
|
|
AlertTitle,
|
|
Box,
|
|
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 { GrainOptions } from "grain";
|
|
import CompModes from "component/ComponentMode.json";
|
|
import FanPicker from "fans/fanPicker";
|
|
import CustomGrainSelector from "grain/CustomGrainSelector";
|
|
|
|
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 [useCustomGrain, setUseCustomGrain] = useState<boolean>(false)
|
|
//const [numCalibrations, setNumCalibrations] = useState(0)
|
|
|
|
useEffect(() => {
|
|
let formComponent = cloneDeep(component);
|
|
let describer = describeMeasurement(
|
|
primaryMeasurement(formComponent.settings.type),
|
|
formComponent.settings.type,
|
|
formComponent.settings.subtype,
|
|
undefined,
|
|
user
|
|
);
|
|
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 (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
|
length = length / 100;
|
|
width = width / 100;
|
|
height = height / 100;
|
|
} else if (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
|
length = length / 30.48;
|
|
width = width / 30.48;
|
|
height = height / 30.48;
|
|
}
|
|
|
|
let sensorDistance = formComponent.settings.sensorDistanceCm ?? 0;
|
|
|
|
if (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
|
sensorDistance = sensorDistance / 100;
|
|
} else if (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
|
sensorDistance = sensorDistance / 30.48;
|
|
}
|
|
|
|
if(formComponent.settings.customGrain){
|
|
setUseCustomGrain(true)
|
|
}
|
|
|
|
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 toggleReverseNodes = (event: any) => {
|
|
let f = cloneDeep(form);
|
|
f.component.settings.reverseNodes = event.target.checked;
|
|
setForm(f);
|
|
};
|
|
|
|
const updateCustomGrain = (grainSettings?: pond.GrainSettings) => {
|
|
let f = cloneDeep(form)
|
|
f.component.settings.customGrain = grainSettings
|
|
f.component.settings.grainType = pond.Grain.GRAIN_CUSTOM
|
|
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,
|
|
undefined,
|
|
user
|
|
).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,
|
|
undefined,
|
|
user
|
|
).toStored(+event.target.value);
|
|
}
|
|
setForm(f);
|
|
};
|
|
|
|
const updateCalibrationTypes = (event: any, index: number) => {
|
|
let f = cloneDeep(form);
|
|
f.component.settings.calibrations[index].measurementType = 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 (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
|
value = value * 100;
|
|
} else if (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
|
value = value * 30.48;
|
|
}
|
|
if (dimensions !== undefined && dimensions !== null) {
|
|
dimensions.lengthCm = value;
|
|
} else {
|
|
dimensions = quack.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 (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
|
value = value * 100;
|
|
} else if (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
|
value = value * 30.48;
|
|
}
|
|
if (dimensions !== undefined && dimensions !== null) {
|
|
dimensions.heightCm = value;
|
|
} else {
|
|
dimensions = quack.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 (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
|
value = value * 100;
|
|
} else if (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET) {
|
|
value = value * 30.48;
|
|
}
|
|
if (dimensions !== undefined && dimensions !== null) {
|
|
dimensions.widthCm = value;
|
|
} else {
|
|
dimensions = quack.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 (user.distanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_METERS) {
|
|
value = value * 100;
|
|
} else if (user.distanceUnit() === 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 (
|
|
<React.Fragment>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
value={useCustomGrain}
|
|
checked={useCustomGrain}
|
|
title="Custom Grain"
|
|
onClick={() => {
|
|
setUseCustomGrain(!useCustomGrain);
|
|
updateCustomGrain()
|
|
}}
|
|
/>
|
|
}
|
|
label="Custom Grain"
|
|
labelPlacement="start"
|
|
/>
|
|
{useCustomGrain ?
|
|
<Box sx={{border: "1px solid white", borderRadius: 2, padding: 2, marginBottom: 2}}>
|
|
<CustomGrainSelector onGrainSettingsChange={updateCustomGrain} initialGrain={form.component.customGrainProps()}/>
|
|
</Box>
|
|
:
|
|
<SearchSelect
|
|
selected={selected}
|
|
changeSelection={updateGrainType}
|
|
label="Grain Type"
|
|
options={grainOptions}
|
|
group
|
|
/>
|
|
}
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
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,
|
|
undefined,
|
|
user
|
|
).unit()}
|
|
</InputAdornment>
|
|
)
|
|
}}></TextField>
|
|
</Grid>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const componentMode = () => {
|
|
const { component, coefficient, offset } = form;
|
|
return (
|
|
<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>
|
|
)
|
|
}
|
|
|
|
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
|
|
const { component } = form;
|
|
return describeMeasurement(measurementType, component.type(), component.subType(), undefined, user);
|
|
};
|
|
|
|
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.measurementType ?? 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(
|
|
quack.Calibration.create({
|
|
measurementType: quack.MeasurementType.MEASUREMENT_TYPE_INVALID,
|
|
calibrationOffset: 0,
|
|
calibrationCoefficient: 0
|
|
})
|
|
);
|
|
setForm(f);
|
|
}}>
|
|
Add Calibration
|
|
</Button>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const reverseCheck = () => {
|
|
const {reverseNodes} = form.component.settings
|
|
let ext = extension(component.type(), component.subType())
|
|
|
|
if (ext.isArray && device.featureSupported("reverseNodes")) {
|
|
return (
|
|
<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={reverseNodes}
|
|
onChange={toggleReverseNodes}
|
|
name="nodesReversed"
|
|
aria-label="nodesReversed"
|
|
color="secondary"
|
|
/>
|
|
}
|
|
label={<Typography variant="caption">Reverse Nodes</Typography>}
|
|
labelPlacement="top"
|
|
className={classes.switchControl}
|
|
disabled={!canEdit}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
)
|
|
}
|
|
return
|
|
}
|
|
|
|
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()}
|
|
{reverseCheck()}
|
|
{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>
|
|
}
|
|
{device.featureSupported("individualCalibrations") ? (
|
|
<React.Fragment>
|
|
{compMode && componentMode()}
|
|
{individualCalibration()}
|
|
</React.Fragment>
|
|
) : (
|
|
!compMode ? (
|
|
blanketCalibration()
|
|
):(
|
|
componentMode()
|
|
)
|
|
)}
|
|
<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">
|
|
{user.distanceUnit() === 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">
|
|
{user.distanceUnit() === 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">
|
|
{user.distanceUnit() === 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">
|
|
{user.distanceUnit() === 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>;
|
|
}
|