Merge branch 'email_control' into staging_environment
This commit is contained in:
commit
64fcc934ce
3 changed files with 295 additions and 36 deletions
|
|
@ -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<ComponentData>({
|
||||
|
|
@ -137,7 +139,8 @@ export default function ComponentForm(props: Props) {
|
|||
sensorDistance: "0",
|
||||
});
|
||||
const [compMode, setCompMode] = useState<any>();
|
||||
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(false)
|
||||
const [useCustomGrain, setUseCustomGrain] = useState<boolean>(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 && (
|
||||
<React.Fragment>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<TextField
|
||||
|
|
@ -1382,6 +1400,62 @@ export default function ComponentForm(props: Props) {
|
|||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Typography variant="subtitle2" style={{ marginTop: 16, marginBottom: 8 }}>
|
||||
Authorized Control Emails
|
||||
</Typography>
|
||||
<Grid container spacing={1} alignItems="center">
|
||||
<Grid size={{ xs: 9 }}>
|
||||
<TextField
|
||||
label="Email address"
|
||||
type="email"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={controlEmail}
|
||||
onChange={e => setControlEmail(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addControlEmail();
|
||||
}
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={addControlEmail}
|
||||
disabled={!canEdit || controlEmail.trim() === ""}>
|
||||
Add
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{component.settings.controlEmails.map((email, i) => (
|
||||
<Grid
|
||||
container
|
||||
key={i}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
style={{ marginTop: 4 }}>
|
||||
<Grid>
|
||||
<Typography variant="body2">{email}</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => removeControlEmail(i)}
|
||||
disabled={!canEdit}
|
||||
className={classes.redButton}>
|
||||
<RemoveIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{showDataUsageWarning && (
|
||||
|
|
|
|||
|
|
@ -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<number[]>([]);
|
||||
const [nodeToExclude, setNodeToExclude] = useState(0);
|
||||
const [controlEmail, setControlEmail] = useState("");
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setTabVal(newValue);
|
||||
|
|
@ -499,6 +503,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);
|
||||
|
|
@ -870,6 +875,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 (
|
||||
<DialogContent>
|
||||
<TextField
|
||||
id="output-mode"
|
||||
name="outputMode"
|
||||
select
|
||||
label="Output Mode"
|
||||
value={formComponent.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>
|
||||
<FormControl fullWidth>
|
||||
<PeriodSelect
|
||||
id="minCycleTimeMs"
|
||||
label="Minimum cycle time"
|
||||
units={["seconds", "minutes"]}
|
||||
isDisabled={!canEdit}
|
||||
isError={!isMinCycleTimeValid()}
|
||||
initialMs={
|
||||
formComponent.settings.minCycleTimeMs
|
||||
? formComponent.settings.minCycleTimeMs
|
||||
: undefined
|
||||
}
|
||||
onChange={handleMinCycleTimeChanged}
|
||||
helperText={
|
||||
isMinCycleTimeValid()
|
||||
? "The minimum amount of time that the component spends in a state"
|
||||
: minCycleTimeDescription()
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<Typography variant="subtitle2" sx={{ marginTop: 2, marginBottom: 1 }}>
|
||||
Authorized Control Emails
|
||||
</Typography>
|
||||
<Grid container spacing={1} alignItems="center">
|
||||
<Grid size={{ xs: 9 }}>
|
||||
<TextField
|
||||
label="Email address"
|
||||
type="email"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
value={controlEmail}
|
||||
onChange={e => setControlEmail(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addControlEmail();
|
||||
}
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={addControlEmail}
|
||||
disabled={!canEdit || controlEmail.trim() === ""}>
|
||||
Add
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{formComponent.settings.controlEmails.map((email, i) => (
|
||||
<Grid
|
||||
container
|
||||
key={i}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
style={{ marginTop: 4 }}>
|
||||
<Grid>
|
||||
<Typography variant="body2">{email}</Typography>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => removeControlEmail(i)}
|
||||
disabled={!canEdit}
|
||||
className={classNames(classes.redButton)}>
|
||||
<RemoveIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
))}
|
||||
<Divider sx={{ marginTop: 3, marginBottom: 2 }} />
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Email Control
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
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:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "action.hover",
|
||||
borderRadius: 1,
|
||||
padding: 1.5,
|
||||
marginTop: 1,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.85rem"
|
||||
}}>
|
||||
<div>{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - ON</div>
|
||||
<div>{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - OFF</div>
|
||||
<div>{device.id()}:{formComponent.settings.type}-{formComponent.settings.addressType}-{formComponent.settings.address} - AUTO</div>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
);
|
||||
};
|
||||
|
||||
const actions = () => {
|
||||
return (
|
||||
<DialogActions>
|
||||
|
|
@ -1045,36 +1211,55 @@ export default function ComponentSettings(props: Props) {
|
|||
scroll="paper">
|
||||
{title()}
|
||||
<Divider />
|
||||
{!isController(formComponent.settings.type) && (
|
||||
<Tabs
|
||||
value={tabVal}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={handleChange}>
|
||||
<Tab label="General" />
|
||||
<Tab label="Overlays" />
|
||||
<Tab label="Nodes" disabled={!prevComponent} />
|
||||
</Tabs>
|
||||
{isController(formComponent.settings.type) ? (
|
||||
<React.Fragment>
|
||||
<Tabs
|
||||
value={tabVal}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={handleChange}>
|
||||
<Tab label="General" />
|
||||
<Tab label="Control" />
|
||||
</Tabs>
|
||||
<TabPanelMine value={tabVal} index={0}>
|
||||
{content()}
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={tabVal} index={1}>
|
||||
{controlContent()}
|
||||
</TabPanelMine>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Tabs
|
||||
value={tabVal}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
onChange={handleChange}>
|
||||
<Tab label="General" />
|
||||
<Tab label="Overlays" />
|
||||
<Tab label="Nodes" disabled={!prevComponent} />
|
||||
</Tabs>
|
||||
<TabPanelMine value={tabVal} index={0}>
|
||||
{content()}
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={tabVal} index={1}>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
spacing={1}>
|
||||
{overlayGroup()}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={tabVal} index={2}>
|
||||
{componentPrefs()}
|
||||
</TabPanelMine>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<TabPanelMine value={tabVal} index={0}>
|
||||
{content()}
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={tabVal} index={1}>
|
||||
<DialogContent>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
spacing={1}>
|
||||
{overlayGroup()}
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={tabVal} index={2}>
|
||||
{componentPrefs()}
|
||||
</TabPanelMine>
|
||||
{actions()}
|
||||
</ResponsiveDialog>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue