frontend/src/component/ComponentSettings.tsx

1268 lines
42 KiB
TypeScript

import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
FormControl,
FormControlLabel,
Grid2 as Grid,
IconButton,
MenuItem,
Step,
StepLabel,
Stepper,
Switch,
Tab,
Tabs,
TextField,
Theme,
Tooltip,
Typography
} from "@mui/material";
import DeleteButton from "common/DeleteButton";
import ResponsiveDialog from "common/ResponsiveDialog";
import SearchSelect, { Option } from "common/SearchSelect";
import { useComponentAPI, useDeviceAPI, usePrevious, useSnackbar } from "hooks";
import { cloneDeep } from "lodash";
import { Component, Device } from "models";
import {
getFriendlyAddressTypeName,
getHumanReadableAddress,
isSingular
} from "pbHelpers/AddressType";
import {
extension,
FilterSubtypeI2C,
getAddressTypes,
GetComponentTypeOptions,
getFriendlyName,
getMeasurements,
isController,
} from "pbHelpers/ComponentType";
import PeriodSelect from "common/time/PeriodSelect";
import { bestUnit, milliToX } from "common/time/duration";
import {
ComponentAvailabilityMap,
DeviceAvailabilityMap,
OffsetAvailabilityMap
} from "pbHelpers/DeviceAvailability";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import React, { useCallback, useEffect, useState } from "react";
import { or } from "utils/types";
import { ConfigurablePin } from "pbHelpers/AddressTypes";
import ColourPicker from "common/ColourPicker";
import classNames from "classnames";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import ComponentForm from "./ComponentForm";
import { green, red } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import {
Remove as RemoveIcon,
Add as AddIcon,
} from "@mui/icons-material"
import CancelSubmit from "common/CancelSubmit";
import { useGlobalState } from "providers";
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;
isDialogOpen: boolean;
closeDialogCallback: () => void;
availablePositions: DeviceAvailabilityMap;
availableOffsets: OffsetAvailabilityMap; //deprecated: this is no longer used with the change of cable id to be a number entry
mode?: "add" | "remove" | "update";
refreshCallback: () => void;
canEdit: boolean;
addressTypeRestriction?: quack.AddressType;
deviceComponentPrefs?: pond.DeviceComponentPreferences;
}
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
export default function ComponentSettings(props: Props) {
const classes = useStyles();
const componentAPI = useComponentAPI();
const deviceAPI = useDeviceAPI();
const { error, warning, success } = useSnackbar();
const {
device,
isDialogOpen,
closeDialogCallback,
availablePositions,
mode,
refreshCallback,
canEdit,
addressTypeRestriction,
deviceComponentPrefs
} = props;
const [{as}] = useGlobalState();
const prevComponent = usePrevious(props.component);
const prevIsDialogOpen = usePrevious(isDialogOpen);
const steps = [0, 1];
const [activeStep, setActiveStep] = useState(0);
const [componentType, setComponentType] = useState<Option | null | undefined>();
const componentTypeOptions: Option[] = GetComponentTypeOptions(addressTypeRestriction);
const [removeDialogOpen, setRemoveDialogOpen] = useState(false);
const [formComponent, setFormComponent] = useState<Component>(new Component());
const [cableID, setCableID] = useState(0);
const [expansionLine, setExpansionLine] = useState(0);
const [overlayIndex, setOverlayIndex] = useState(0);
const [overlayColourDialog, setOverlayCoulourDialog] = useState(false);
const [tabVal, setTabVal] = useState(0);
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);
};
// useEffect(() => {
// if (deviceComponentPrefs) {
// setExcludedNodes(deviceComponentPrefs.excludedNodes);
// }
// }, [deviceComponentPrefs]);
const init = useCallback(() => {
let component = props.component;
let initComponent = new Component();
if (component) {
initComponent.settings = cloneDeep(component.settings);
initComponent.status = cloneDeep(component.status);
}
if (
initComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE &&
initComponent.settings.grainType === pond.Grain.GRAIN_INVALID
) {
initComponent.settings.grainType = pond.Grain.GRAIN_NONE;
}
setComponentType(
componentTypeOptions.find(
t =>
t.value ===
initComponent.settings.type.toString() + ":" + initComponent.settings.subtype.toString()
)
);
setFormComponent(initComponent);
setExcludedNodes(initComponent.settings.excludedNodes)
}, [props.component, componentTypeOptions]);
useEffect(() => {
if (isDialogOpen && prevIsDialogOpen) return;
if (props.component !== prevComponent || isDialogOpen !== prevIsDialogOpen) {
init();
}
}, [props.component, prevComponent, init, isDialogOpen, prevIsDialogOpen]);
const close = () => {
closeDialogCallback();
};
const isFormValid = () => {
//let { component } = form;
let type = or(formComponent.settings.type, "");
if(supportsExpansion() && expansionLine === 0){
return false
}
return isComponentTypeValid(type) && isAddressValid(formComponent);
};
const isComponentTypeValid = (componentType: quack.ComponentType) => {
if (!componentType) {
return false;
}
return true;
};
const getAvailablePositions = (
addressType: quack.AddressType,
componentType: quack.ComponentType,
subtype: number
): number[] => {
let positions = availablePositions.get(addressType);
if (!positions) return [];
switch (addressType) {
case quack.AddressType.ADDRESS_TYPE_I2C:
case quack.AddressType.ADDRESS_TYPE_SPI:
let componentPositions;
if (positions) {
componentPositions = (positions as ComponentAvailabilityMap).get(componentType); //gets the addresses that are not claimed yet
componentPositions = FilterSubtypeI2C(componentType, subtype, componentPositions as number[]) //filters the I2C addresses for valid ones for the subtype
}
return componentPositions ? (componentPositions as number[]) : [];
case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
return positions ? (positions as ConfigurablePin[]).map(p => p.address) : [];
default:
return positions ? (positions as number[]) : [];
}
};
const isAddressValid = (component: Component) => {
let availablePosition = true;
if (mode === "add") {
let positions = getAvailablePositions(
component.settings.addressType,
component.settings.type,
component.settings.subtype
);
availablePosition = positions ? positions.length > 0 : false;
}
return (
availablePosition &&
(isSingular(component.settings.addressType, component.settings.type) ||
or(component.settings.address, 0) >= 0)
);
};
const hasAvailablePositions = (component: Component) => {
// return true // TODO: actually fix this
if (component.settings.addressType === quack.AddressType.ADDRESS_TYPE_INCREMENTAL) {
return true;
}
const addressTypes: quack.AddressType[] = getAddressTypes(
component.settings.type,
component.settings.subtype
);
for (let i = 0; i < addressTypes.length; i++) {
if (getAvailablePositions(addressTypes[i], component.settings.type, component.settings.subtype).length > 0) {
return true;
}
}
return false;
};
const stepBack = () => {
setActiveStep(activeStep - 1);
};
const stepNext = () => {
setActiveStep(activeStep + 1);
};
const refresh = () => {
if (refreshCallback) {
refreshCallback();
}
};
const addComponent = () => {
const component = formComponent;
//component.settings.calibrationCoefficient = Number(form.coefficient);
if (cableID > 0) component.settings.addressType = cableID + 8;
component.settings.expansionLine = expansionLine
componentAPI
.add(device.id(), component.settings, as)
.then((_response: any) => {
success(component.name() + " was successfully added!");
refresh();
})
.catch((_err: any) => {
error("Error occured while adding " + component.name() + ".");
})
.finally(() => close());
};
const updateComponent = () => {
const component = formComponent;
component.settings.excludedNodes = excludedNodes
componentAPI
.update(device.id(), component.settings, getContextKeys(), getContextTypes(), as)
.then((_response: any) => {
success(component.name() + " was successfully updated!");
// let updatedPrefs = pond.DeviceComponentPreferences.create();
// updatedPrefs.excludedNodes = excludedNodes;
// deviceAPI
// .updateComponentPreferences(device.id(), component.key(), updatedPrefs, undefined, undefined, as)
// .then(_resp => {
// console.log("Preferences updated");
// })
// .catch(_err => {
// console.log("Preferences failed to update");
// });
refresh();
})
.catch((_err: any) => {
error("Error occured while updating " + component.name() + ".");
})
.finally(() => {
close();
});
};
const removeComponent = () => {
componentAPI
.remove(device.id(), formComponent.key(), getContextKeys(), getContextTypes(), as)
.then((_response: any) => {
success(formComponent.name() + " was successfully removed!");
refresh();
})
.catch((err: any) => {
err.response.data.error
? warning(err.response.data.error)
: error("Error occured while removing " + formComponent.name() + ".");
})
.finally(() => {
close();
});
};
const handleTypeChanged = (option: Option | null) => {
setComponentType(option);
if (!option) {
formComponent.settings = pond.ComponentSettings.create();
} else {
let rawType = option.value.split(":");
let type = quack.ComponentType[rawType[0] as keyof typeof quack.ComponentType];
let subtype = parseInt(rawType[1], 10);
let ext = extension(type, subtype);
formComponent.settings.type = type;
formComponent.settings.subtype = subtype;
formComponent.settings.addressType = or(addressTypeRestriction, getAddressTypes(
formComponent.settings.type,
formComponent.settings.subtype
)[0]);
formComponent.settings.address = or(
getAvailablePositions(formComponent.settings.addressType, formComponent.settings.type, formComponent.settings.subtype)[0],
Component.create().settings.address
);
formComponent.settings.name = option.label;
if (type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
formComponent.settings.grainType = pond.Grain.GRAIN_NONE;
}
let periodMs = Math.max(60000, ext.minMeasurementPeriodMs);
formComponent.settings.measurementPeriodMs = periodMs;
formComponent.settings.reportPeriodMs = periodMs;
if (ext.isController && ext.minCycleTimeMs) {
formComponent.settings.minCycleTimeMs = ext.minCycleTimeMs;
}
//if there is a default cable id set it here
if (option.defaultCableID) {
setCableID(option.defaultCableID);
}
}
};
const handlePositionChanged = (event: any) => {
setCableID(0)
setExpansionLine(0)
let f = cloneDeep(formComponent);
//split the string
let split: string[] = event.target.value.toString().split(":")
// pop the addres off of the end of the array
f.settings.address = parseInt(split.pop() ?? "0")
// pop the address type out of the array next
f.settings.addressType = parseInt(split.pop() ?? "0")
// f.settings.address = event.target.value
// .toString()
// .split(":")
// .pop();
setFormComponent(f);
};
const getAvailableMenuItemPositions = (type: quack.ComponentType, subtype: number) => {
const addressTypes = getAddressTypes(type, subtype);
let availableMenuItemPositions = [];
for (let i = 0; i < addressTypes.length; i++) {
let addressType = addressTypes[i];
let availablePositions = getAvailablePositions(addressType, type, subtype);
availableMenuItemPositions.push(
<MenuItem key={addressType} divider disabled>
{getFriendlyAddressTypeName(addressType)}
</MenuItem>
);
for (let j = 0; j < availablePositions.length; j++) {
availableMenuItemPositions.push(
<MenuItem
key={i.toString() + "-" + j.toString()}
value={addressType + ":" + availablePositions[j].toString()}>
{getHumanReadableAddress(
addressType,
type,
availablePositions[j],
device.settings.product
)}
</MenuItem>
);
}
}
return availableMenuItemPositions;
};
const toggleOverlays = () => {
let f = cloneDeep(formComponent);
f.settings.hasOverlays = !f.settings.hasOverlays;
setFormComponent(f);
};
const title = () => {
if (mode === "add") {
return (
<DialogTitle id="add-component-dialog-title">
Add Component
<Typography variant="body2" color="textSecondary">
{device.name()} - ID: {device.id()}
</Typography>
</DialogTitle>
);
}
return (
<DialogTitle id="component-settings-dialog-title">
{formComponent.name()} Settings
<Typography variant="body2" color="textSecondary">
{formComponent.subTypeName()}
</Typography>
<Typography variant="body2" color="textSecondary">
{device.name()} - ID: {device.id()}
</Typography>
</DialogTitle>
);
};
const content = () => {
if (mode === "add") {
return addForm();
}
return (
<DialogContent>
<ComponentForm
component={formComponent}
device={device}
canEdit={canEdit}
hideControllerFields={isController(formComponent.settings.type)}
componentChanged={(formComp, isValid) => {
formComponent.settings = formComp.settings;
setFormValid(isValid);
}}
/>
</DialogContent>
);
};
const hasCableID = () => {
return (formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
formComponent.settings.type ===
quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_PRESSURE_CABLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE ||
(formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE &&
formComponent.subType() ===
quack.CapacitorCableSubtype.CAPACITOR_CABLE_SUBTYPE_FROG))
}
const supportsExpansion = () => {
return ((formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE ||
formComponent.settings.type === quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE) &&
formComponent.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C)
}
const addForm = () => {
return (
<DialogContent>
{activeStep === 0 ? (
<React.Fragment>
<SearchSelect
selected={componentType}
changeSelection={handleTypeChanged}
label="Type"
options={componentTypeOptions}
group
/>
<TextField
select
fullWidth
required
id="position"
name="position"
label="Position"
value={
formComponent.settings.addressType + ":" + formComponent.settings.address.toString()
}
onChange={handlePositionChanged}
margin="normal"
autoFocus={false}
disabled={
!isAddressValid(formComponent) ||
isSingular(formComponent.settings.addressType, formComponent.settings.type)
}
helperText={
isComponentTypeValid(formComponent.settings.type)
? hasAvailablePositions(formComponent)
? isSingular(formComponent.settings.addressType, formComponent.settings.type) ||
formComponent.settings.address >= 0
? ""
: "* Please select a position *"
: "No positions available for " +
getFriendlyName(formComponent.settings.type) +
" components"
: "* Position depends on the type *"
}
error={
isComponentTypeValid(formComponent.settings.type) &&
!hasAvailablePositions(formComponent)
}
variant="outlined"
InputLabelProps={{ shrink: true }}
SelectProps={{ className: classes.selectInput }}>
{getAvailableMenuItemPositions(
formComponent.settings.type,
formComponent.settings.subtype
)}
</TextField>
{supportsExpansion() && setExpLine()}
{(hasCableID() && !supportsExpansion())&& setComponentAddrType()}
</React.Fragment>
) : activeStep === 1 ? (
<ComponentForm
component={formComponent}
device={device}
canEdit={canEdit}
componentChanged={(formComp, isValid) => {
formComponent.settings = formComp.settings;
setFormValid(isValid);
}}
/>
) : null}
<Stepper activeStep={activeStep} className={classes.stepper}>
<Step key={0}>
<StepLabel>Required</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={
<Typography variant="caption" color="textSecondary">
Optional
</Typography>
}>
General
</StepLabel>
</Step>
</Stepper>
</DialogContent>
);
};
const setExpLine = () => {
return (
<TextField
label="Expansion Line"
type="number"
margin="normal"
error={expansionLine < 1 || expansionLine > 12}
fullWidth
variant="outlined"
value={expansionLine}
helperText="Enter line the cable is connected to on your expander"
onChange={e => {
let number = parseInt(e.target.value);
if (number < 0 || isNaN(number)) number = 0;
if (number > 12) number = 12;
setExpansionLine(number);
}}
/>
);
};
const setComponentAddrType = () => {
return (
<TextField
label="Cable ID"
type="number"
margin="normal"
fullWidth
variant="outlined"
value={cableID}
InputLabelProps={{ shrink: true }}
helperText="Enter cable ID, 0 for auto"
onChange={e => {
let number = parseInt(e.target.value);
if (number < 0 || isNaN(number)) number = 0;
if (number > 100) number = 100;
setCableID(number);
}}
/>
);
};
const createNewOverlay = () => {
let f = cloneDeep(formComponent);
f.settings.overlays.push(
pond.ComponentOverlays.create({
min: 0,
max: 0,
colour: "#" + Math.floor(Math.random() * 16777215).toString(16),
message: "",
measurementType: getMeasurements(formComponent.type(), formComponent.subType())[0]
.measurementType
})
);
setFormComponent(f);
};
const removeOverlay = (index: number) => {
let f = cloneDeep(formComponent);
f.settings.overlays.splice(index, 1);
setFormComponent(f);
};
const changeOverlayMin = (index: number, min: number) => {
let f = cloneDeep(formComponent);
f.settings.overlays[index].min = min;
setFormComponent(f);
};
const changeOverlayMax = (index: number, max: number) => {
let f = cloneDeep(formComponent);
f.settings.overlays[index].max = max;
setFormComponent(f);
};
const changeOverlayMessage = (index: number, message: string) => {
let f = cloneDeep(formComponent);
f.settings.overlays[index].message = message;
setFormComponent(f);
};
const changeOverlayColour = (index: number, colour: string) => {
let f = cloneDeep(formComponent);
f.settings.overlays[index].colour = colour;
setFormComponent(f);
};
const changeMeasurementType = (index: number, type: quack.MeasurementType) => {
let f = cloneDeep(formComponent);
f.settings.overlays[index].measurementType = type;
setFormComponent(f);
};
const overlayGroup = () => {
//const { component } = form;
let types = getMeasurements(formComponent.type(), formComponent.subType());
const groups = [];
for (let i = 0; i < formComponent.settings.overlays.length; i++) {
groups[i] = (
<React.Fragment key={"overlay" + i}>
<Grid size={{ xs: 6, sm: 6 }}>
<TextField
id="min"
label="Minimum"
disabled={!formComponent.settings.hasOverlays}
value={formComponent.settings.overlays[i].min}
onChange={e => changeOverlayMin(i, +e.target.value)}
margin="normal"
fullWidth
type="number"
variant="outlined"
/>
<TextField
id="max"
label="Maximum"
disabled={!formComponent.settings.hasOverlays}
value={formComponent.settings.overlays[i].max}
onChange={e => changeOverlayMax(i, +e.target.value)}
margin="normal"
fullWidth
type="number"
variant="outlined"
/>
<TextField
id="message"
label="message"
disabled={!formComponent.settings.hasOverlays}
value={formComponent.settings.overlays[i].message}
onChange={e => changeOverlayMessage(i, e.target.value)}
margin="normal"
fullWidth
type="text"
variant="outlined"
/>
</Grid>
<Grid size={{ xs: 6, sm: 6 }}>
<Grid container alignContent="center">
<Grid size={{ xs: 12, sm: 12 }} style={{ marginBottom: 30 }}>
<TextField
select
id="measurementType"
required={true}
helperText="Measurement"
disabled={!formComponent.settings.hasOverlays}
value={formComponent.settings.overlays[i].measurementType}
onChange={event => changeMeasurementType(i, +event.target.value)}
autoFocus={false}
margin="dense"
variant="standard"
fullWidth
InputLabelProps={{ shrink: true }}>
{types.map(type => (
<MenuItem key={type.measurementType} value={type.measurementType}>
{type.label}
</MenuItem>
))}
</TextField>
</Grid>
<Grid size={{ xs: 12, sm: 12 }}>
<Box
width={"100%"}
height={50}
marginBottom={3}
style={{
background: formComponent.settings.overlays[i].colour,
cursor: "pointer"
}}
onClick={() => {
setOverlayIndex(i);
setOverlayCoulourDialog(true);
}}
/>
</Grid>
<Grid size={{ xs: 12, sm: 12 }}>
<Tooltip title="Remove Overlay">
<Box width={"100%"} style={{ textAlign: "center", padding: 10 }}>
<IconButton
color="default"
disabled={!formComponent.settings.hasOverlays || !canEdit}
aria-label="Remove overlay"
onClick={() => removeOverlay(i)}
className={classNames(classes.redButton)}>
<RemoveIcon />
</IconButton>
</Box>
</Tooltip>
</Grid>
</Grid>
</Grid>
</React.Fragment>
);
}
return (
<React.Fragment>
<Grid size={{ xs: 12, sm: 12 }}>
<Grid container alignItems="center">
<Grid size={{ xs: 6, sm: 6 }}>
<Tooltip
title="Creates an indicator for when measurements are within a given range"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={formComponent.settings.hasOverlays}
onChange={toggleOverlays}
name="overlays"
aria-label="overlays"
color="secondary"
/>
}
label="Overlays"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 6, sm: 6 }}>
<Tooltip title="Create New Overlay">
<div>
<IconButton
color="primary"
disabled={!formComponent.settings.hasOverlays || !canEdit}
aria-label="Add another overlay"
onClick={createNewOverlay}
className={classNames(classes.greenButton, classes.noPadding)}>
<AddIcon />
</IconButton>
</div>
</Tooltip>
</Grid>
</Grid>
</Grid>
{groups}
{formComponent.settings.overlays[overlayIndex] && (
<ResponsiveDialog
open={overlayColourDialog}
onClose={() => setOverlayCoulourDialog(false)}>
<DialogContent>
<Box width={200}>
<ColourPicker
colour={formComponent.settings.overlays[overlayIndex].colour}
onChange={colour => changeOverlayColour(overlayIndex, colour)}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOverlayCoulourDialog(false)} color="primary">
Done
</Button>
</DialogActions>
</ResponsiveDialog>
)}
</React.Fragment>
);
};
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>
<Grid container justifyContent="space-between" direction="row" width={"100%"}>
<Grid size={{ xs: 5 }} >
{mode === "add" ? (
<Button disabled={activeStep === 0} onClick={stepBack} color="primary">
Back
</Button>
) : (
<DeleteButton disabled={!canEdit} onClick={() => setRemoveDialogOpen(true)}>Delete</DeleteButton>
)}
</Grid>
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
{mode === "add" ? (
<React.Fragment>
<Button onClick={close} color="primary">
<Typography variant="subtitle2" color="textPrimary">
Cancel
</Typography>
</Button>
{activeStep === steps.length - 1 ? (
<Button color="primary" onClick={addComponent} disabled={!formValid}>
Submit
</Button>
) : (
<Button color="primary" onClick={stepNext} disabled={!isFormValid()}>
Next
</Button>
)}
</React.Fragment>
) : (
<React.Fragment>
<CancelSubmit onSubmit={updateComponent} onCancel={close} submitDisabled={!canEdit || !formValid}/>
</React.Fragment>
)}
</Grid>
</Grid>
</DialogActions>
);
};
const remove = () => {
return (
<Dialog
open={isDialogOpen}
onClose={close}
aria-labelledby="confirm-remove-component-label"
aria-describedby="confirm-remove-component-description">
<DialogTitle id="confirm-remove-component-title">Delete Confirmation</DialogTitle>
<DialogContent>
<DialogContentText>Are you sure you want to delete this component?</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={close} color="primary">
No
</Button>
<Button onClick={removeComponent} color="primary">
Yes
</Button>
</DialogActions>
</Dialog>
);
};
const componentPrefs = () => {
let nodes;
if (
prevComponent &&
prevComponent.status.measurement[0] &&
prevComponent.status.measurement[0].values[0] &&
prevComponent.status.measurement[0].values[0].values
) {
nodes = prevComponent.status.measurement[0].values[0].values;
}
return (
<Box paddingLeft={2} paddingRight={2}>
<Typography style={{ fontWeight: 650 }}>Nodes to Exclude</Typography>
{/* node exclusion */}
{nodes ? (
<Box>
{/* if we have how many nodes are found use checkboxes for each node to add to what is excluded */}
{nodes.map((_, i) => (
<FormControlLabel
key={i}
control={
<Checkbox
checked={excludedNodes.includes(i)}
onChange={(e, checked) => {
let exclNodes = cloneDeep(excludedNodes);
if (checked) {
exclNodes.push(i);
} else {
exclNodes.splice(exclNodes.indexOf(i), 1);
}
setExcludedNodes(exclNodes);
}}
/>
}
label={"Node " + (i + 1)}
/>
))}
</Box>
) : (
<Box>
<Box marginBottom={2}>
<Typography>
Cannot determine number of nodes as the component has not taken a measurement yet.
If you know the number of nodes and which ones you would like to exclude you may
enter them below. Note that the nodes are 0 based so to exclude the first node you
would enter 0
</Typography>
</Box>
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center">
{/* if we don't have how many nodes the component has use a entry box to enter the number of the node you wnat to exclude */}
<Grid size={{ xs: 12 }}>
<Typography>Excluded Nodes: {excludedNodes.toString()}</Typography>
</Grid>
<Grid>
<TextField
type="number"
variant="outlined"
InputProps={{ inputProps: { min: 0 } }}
value={nodeToExclude}
onChange={e => {
setNodeToExclude(+e.target.value);
}}
/>
</Grid>
<Grid>
<Button
onClick={() => {
let eNodes = cloneDeep(excludedNodes);
if (!eNodes.includes(nodeToExclude)) {
eNodes.push(nodeToExclude);
}
setExcludedNodes(eNodes);
}}>
Add
</Button>
</Grid>
</Grid>
</Box>
)}
<Button
variant="contained"
color="primary"
onClick={() => {
setExcludedNodes([]);
}}>
Clear Excluded Nodes
</Button>
</Box>
);
};
return (
<React.Fragment>
{mode === "remove" || removeDialogOpen
? remove()
: isDialogOpen && (
<ResponsiveDialog
maxWidth="sm"
fullWidth
open={isDialogOpen}
onClose={close}
scroll="paper">
{title()}
<Divider />
{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>
)}
{actions()}
</ResponsiveDialog>
)}
</React.Fragment>
);
}