diff --git a/package-lock.json b/package-lock.json index 58133fb..4d78d05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#control_emails", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", @@ -11198,7 +11198,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#29d9765ce93c04ad70d795b80e9873fc2fc5f600", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#4011e9e7a75148a195e7311c41a4e6c8d6886d7b", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index cd2f476..daa71c4 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "mui-tel-input": "^7.0.0", "notistack": "^3.0.1", "openweathermap-ts": "^1.2.10", - "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master", + "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#control_emails", "query-string": "^9.2.1", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", 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..c6c981b 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,145 @@ 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)}> + + + + + ))} + + ); + }; + const actions = () => { return ( @@ -1042,36 +1186,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()} )}