1267 lines
42 KiB
TypeScript
1267 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(cloneDeep(initComponent.settings.excludedNodes));
|
|
}, [props.component, componentTypeOptions]);
|
|
|
|
useEffect(() => {
|
|
if (!isDialogOpen || prevIsDialogOpen) return;
|
|
init();
|
|
}, [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_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.type === quack.ComponentType.COMPONENT_TYPE_ANALOG_PRESSURE
|
|
) &&
|
|
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>
|
|
);
|
|
}
|