made adjustments to to the grain cable by adding i2c as an option as long as you set and expander line, also ajusted DeviceAvailability so that an i2c address will not be claimed if the component has an expansion line set, updated the device wizard to removed pin options if the i2c port was selected and vice versa for pins since we now have a components that supports both types, updated the components setting to set the default address type based on the restriction passed in that is set by the wizard, and fall back to what is was before if one is not passed in
1091 lines
35 KiB
TypeScript
1091 lines
35 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
Checkbox,
|
|
Dialog,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogContentText,
|
|
DialogTitle,
|
|
Divider,
|
|
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,
|
|
getAddressTypes,
|
|
GetComponentTypeOptions,
|
|
getFriendlyName,
|
|
getMeasurements,
|
|
isController
|
|
} from "pbHelpers/ComponentType";
|
|
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 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);
|
|
}, [props.component, componentTypeOptions]);
|
|
|
|
useEffect(() => {
|
|
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
|
|
): 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);
|
|
}
|
|
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
|
|
);
|
|
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).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;
|
|
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)[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);
|
|
|
|
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}
|
|
componentChanged={(formComp, isValid) => {
|
|
formComponent.settings = formComp.settings;
|
|
setFormValid(isValid);
|
|
}}
|
|
/>
|
|
</DialogContent>
|
|
);
|
|
};
|
|
|
|
const isCableComponent = () => {
|
|
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.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()}
|
|
{(isCableComponent() && !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 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.lastMeasurement[0] &&
|
|
prevComponent.lastMeasurement[0].values[0] &&
|
|
prevComponent.lastMeasurement[0].values[0].values
|
|
) {
|
|
nodes = prevComponent.lastMeasurement[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>
|
|
{/* button for testing the pref update */}
|
|
{/* {prevComponent && (
|
|
<Button
|
|
onClick={() => {
|
|
let pref = pond.DeviceComponentPreferences.create();
|
|
pref.excludedNodes = excludedNodes;
|
|
deviceAPI
|
|
.updateComponentPreferences(device.id(), prevComponent.key(), pref)
|
|
.then(resp => {
|
|
console.log("no errors");
|
|
})
|
|
.catch(err => {
|
|
console.log("errors");
|
|
});
|
|
}}>
|
|
Update Prefs
|
|
</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) && (
|
|
<Tabs
|
|
value={tabVal}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
onChange={handleChange}>
|
|
<Tab label="General" />
|
|
<Tab label="Overlays" />
|
|
<Tab label="Preferences" 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>
|
|
{actions()}
|
|
</ResponsiveDialog>
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
}
|