diff --git a/src/component/ComponentForm.tsx b/src/component/ComponentForm.tsx index 17c1bf7..dbebbb5 100644 --- a/src/component/ComponentForm.tsx +++ b/src/component/ComponentForm.tsx @@ -19,9 +19,10 @@ import { Tooltip, Typography } from "@mui/material"; -import { +import { ExpandMore, - Close as CloseIcon + Close as CloseIcon, + Remove as RemoveIcon } from "@mui/icons-material"; import PeriodSelect from "common/time/PeriodSelect"; import SearchSelect, { Option } from "common/SearchSelect"; @@ -97,6 +98,7 @@ interface Props { component: Component; componentChanged: (component: Component, formValid: boolean) => void; canEdit: boolean; + hideControllerFields?: boolean; } interface ComponentData { @@ -119,7 +121,7 @@ export default function ComponentForm(props: Props) { const classes = useStyles(); const grainOptions = GrainOptions(); const [{ user }] = useGlobalState(); - const { device, component, componentChanged, canEdit } = props; + const { device, component, componentChanged, canEdit, hideControllerFields } = props; const [reportExpanded, setReportExpanded] = useState(false); const [dataUsageWarningDismissed, setDataUsageWarningDismissed] = useState(true); const [form, setForm] = useState({ @@ -137,7 +139,8 @@ export default function ComponentForm(props: Props) { sensorDistance: "0", }); const [compMode, setCompMode] = useState(); - const [useCustomGrain, setUseCustomGrain] = useState(false) + const [useCustomGrain, setUseCustomGrain] = useState(false); + const [controlEmail, setControlEmail] = useState(""); //const [numCalibrations, setNumCalibrations] = useState(0) useEffect(() => { @@ -356,6 +359,21 @@ export default function ComponentForm(props: Props) { setForm(f); }; + const addControlEmail = () => { + const trimmed = controlEmail.trim().toLowerCase(); + if (trimmed === "" || form.component.settings.controlEmails.includes(trimmed)) return; + let f = cloneDeep(form); + f.component.settings.controlEmails.push(trimmed); + setForm(f); + setControlEmail(""); + }; + + const removeControlEmail = (index: number) => { + let f = cloneDeep(form); + f.component.settings.controlEmails.splice(index, 1); + setForm(f); + }; + const toggleMeasure = (event: any) => { let f = cloneDeep(form); f.measure = event.target.checked; @@ -1341,7 +1359,7 @@ export default function ComponentForm(props: Props) { }} /> )} - {isController(component.settings.type) && ( + {isController(component.settings.type) && !hideControllerFields && ( + + + Authorized Control Emails + + + + setControlEmail(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault(); + addControlEmail(); + } + }} + disabled={!canEdit} + /> + + + + + + {component.settings.controlEmails.map((email, i) => ( + + + {email} + + + removeControlEmail(i)} + disabled={!canEdit} + className={classes.redButton}> + + + + + ))} + )} {showDataUsageWarning && ( diff --git a/src/component/ComponentSettings.tsx b/src/component/ComponentSettings.tsx index db0e5ad..e9a7983 100644 --- a/src/component/ComponentSettings.tsx +++ b/src/component/ComponentSettings.tsx @@ -8,6 +8,7 @@ import { DialogContentText, DialogTitle, Divider, + FormControl, FormControlLabel, Grid2 as Grid, IconButton, @@ -41,8 +42,10 @@ import { GetComponentTypeOptions, getFriendlyName, getMeasurements, - isController + isController, } from "pbHelpers/ComponentType"; +import PeriodSelect from "common/time/PeriodSelect"; +import { bestUnit, milliToX } from "common/time/duration"; import { ComponentAvailabilityMap, DeviceAvailabilityMap, @@ -177,6 +180,7 @@ export default function ComponentSettings(props: Props) { const [formValid, setFormValid] = useState(false); const [excludedNodes, setExcludedNodes] = useState([]); const [nodeToExclude, setNodeToExclude] = useState(0); + const [controlEmail, setControlEmail] = useState(""); const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { setTabVal(newValue); @@ -498,6 +502,7 @@ export default function ComponentSettings(props: Props) { component={formComponent} device={device} canEdit={canEdit} + hideControllerFields={isController(formComponent.settings.type)} componentChanged={(formComp, isValid) => { formComponent.settings = formComp.settings; setFormValid(isValid); @@ -867,6 +872,167 @@ export default function ComponentSettings(props: Props) { ); }; + const addControlEmail = () => { + const trimmed = controlEmail.trim().toLowerCase(); + if (trimmed === "" || formComponent.settings.controlEmails.includes(trimmed)) return; + let f = cloneDeep(formComponent); + f.settings.controlEmails.push(trimmed); + setFormComponent(f); + setControlEmail(""); + }; + + const removeControlEmail = (index: number) => { + let f = cloneDeep(formComponent); + f.settings.controlEmails.splice(index, 1); + setFormComponent(f); + }; + + const handleOutputModeChanged = (event: any) => { + let f = cloneDeep(formComponent); + f.settings.defaultOutputState = event.target.value; + setFormComponent(f); + }; + + const handleMinCycleTimeChanged = (ms: number) => { + let f = cloneDeep(formComponent); + f.settings.minCycleTimeMs = Number(ms); + setFormComponent(f); + }; + + const isMinCycleTimeValid = () => { + const ext = extension(formComponent.settings.type, formComponent.settings.subtype); + if (!ext.isController) return true; + let min = Math.max(0, ext.minCycleTimeMs ? ext.minCycleTimeMs : 0); + return formComponent.settings.minCycleTimeMs >= min; + }; + + const minCycleTimeDescription = () => { + const ext = extension(formComponent.settings.type, formComponent.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 controlContent = () => { + return ( + + + Auto + On + Off + + + + + + Authorized Control Emails + + + + setControlEmail(e.target.value)} + onKeyDown={e => { + if (e.key === "Enter") { + e.preventDefault(); + addControlEmail(); + } + }} + disabled={!canEdit} + /> + + + + + + {formComponent.settings.controlEmails.map((email, i) => ( + + + {email} + + + removeControlEmail(i)} + disabled={!canEdit} + className={classNames(classes.redButton)}> + + + + + ))} + + + Email Control + + + Authorized emails can control this component by sending an email to the + control address. Contact your administrator for the control email + address. Use one of the following as the email subject: + + +
{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - ON
+
{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - OFF
+
{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - AUTO
+
+
+ ); + }; + const actions = () => { return ( @@ -1042,36 +1208,55 @@ export default function ComponentSettings(props: Props) { scroll="paper"> {title()} - {!isController(formComponent.settings.type) && ( - - - - - + {isController(formComponent.settings.type) ? ( + + + + + + + {content()} + + + {controlContent()} + + + ) : ( + + + + + + + + {content()} + + + + + {overlayGroup()} + + + + + {componentPrefs()} + + )} - - {content()} - - - - - {overlayGroup()} - - - - - {componentPrefs()} - {actions()} )} diff --git a/src/device/VersionChip.tsx b/src/device/VersionChip.tsx index 09dac0f..8826239 100644 --- a/src/device/VersionChip.tsx +++ b/src/device/VersionChip.tsx @@ -22,6 +22,14 @@ class VersionChip extends React.Component { variant="outlined" label={firmwareVersionHelper.description} icon={firmwareVersionHelper.icon} + sx={ + firmwareVersionHelper.colour + ? { + borderColor: firmwareVersionHelper.colour, + color: firmwareVersionHelper.colour + } + : undefined + } /> ); diff --git a/src/pbHelpers/FirmwareVersion.tsx b/src/pbHelpers/FirmwareVersion.tsx index d7dcc8c..49472be 100644 --- a/src/pbHelpers/FirmwareVersion.tsx +++ b/src/pbHelpers/FirmwareVersion.tsx @@ -9,6 +9,7 @@ export interface FirmwareVersionHelper { const oldFirmwareColour = "var(--status-warning)"; const currentFirmwareColour = "var(--status-ok)"; +const unknownFirmwareColour = "var(--status-warning)"; export function getFirmwareVersionHelper( version: string, @@ -17,24 +18,35 @@ export function getFirmwareVersionHelper( const firmwareIcon = ; const oldFirmwareIcon = ; const currentFirmwareIcon = ; + const unknownFirmwareIcon = ; let helper = {} as FirmwareVersionHelper; if (!version || version === "") { helper.description = "Unknown version"; helper.icon = oldFirmwareIcon; helper.tooltip = "We don't know what version your device is, it may behave unexpectedly"; + helper.colour = oldFirmwareColour; + } else if (version.startsWith("!")) { + const cleanVersion = version.substring(1); + helper.description = cleanVersion; + helper.icon = unknownFirmwareIcon; + helper.tooltip = "Firmware " + cleanVersion + " was reported by the device but is missing from the firmware list"; + helper.colour = unknownFirmwareColour; } else if (available !== "" && version !== available) { helper.description = version; helper.icon = oldFirmwareIcon; helper.tooltip = "A firmware upgrade is available, some features may be disabled for out-of-date devices"; + helper.colour = oldFirmwareColour; } else if (available === "") { helper.description = version; helper.icon = firmwareIcon; helper.tooltip = "Running version " + version; + helper.colour = ""; } else { helper.description = version; helper.icon = currentFirmwareIcon; helper.tooltip = "Your device is running the latest firmware"; + helper.colour = currentFirmwareColour; } return helper; }