switching from having 3 dialogs to using a stepper

This commit is contained in:
csawatzky 2025-11-20 10:26:33 -06:00
parent 283958ab6a
commit 70bd91bbe7
4 changed files with 576 additions and 285 deletions

View file

@ -3,6 +3,7 @@ import {
Box, Box,
Button, Button,
Card, Card,
CircularProgress,
darken, darken,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -241,7 +242,7 @@ export default function BinVisualizer(props: Props) {
).colour(); ).colour();
const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour(); const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour();
const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour(); const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour();
const [showInputMoisture, setShowInputMoisture] = useState<boolean>(false); // const [showInputMoisture, setShowInputMoisture] = useState<boolean>(false);
const [moistureInput, setMoistureInput] = useState<string>(""); const [moistureInput, setMoistureInput] = useState<string>("");
const [tempInput, setTempInput] = useState<string>(""); const [tempInput, setTempInput] = useState<string>("");
const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>(""); const [outdoorHumidityInput, setOutdoorHumidityInput] = useState<string>("");
@ -295,55 +296,7 @@ export default function BinVisualizer(props: Props) {
const [combinedPlenums, setCombinedPlenums] = useState<CombinedPlenum[]>([]) const [combinedPlenums, setCombinedPlenums] = useState<CombinedPlenum[]>([])
const [openModeChange, setOpenModeChange] = useState(false) const [openModeChange, setOpenModeChange] = useState(false)
const [modeChangeInProgress, setModeChangeInProgress] = useState(false)
// const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// }));
// const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }));
useEffect(() => { useEffect(() => {
setModeTime(moment(bin.status.lastModeChange)); setModeTime(moment(bin.status.lastModeChange));
@ -429,20 +382,6 @@ export default function BinVisualizer(props: Props) {
humids.push(...filteredNodes.humids) humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures) emcs.push(...filteredNodes.moistures)
} }
// let tempClone = cloneDeep(cable.temperatures).reverse();
// let humClone = cloneDeep(cable.humidities).reverse();
// let emcClone = cloneDeep(cable.grainMoistures).reverse();
//add the cable data to the proper arrays
// if (cable.topNode > 0) {
// temps.push(...tempClone.splice(0, cable.topNode));
// humids.push(...humClone.splice(0, cable.topNode));
// emcs.push(...emcClone.splice(0, cable.topNode));
// } else {
// //if the cable has no fill set (top node) then use all of the nodes
// temps = tempClone;
// humids = humClone;
// emcs = emcClone;
// }
//add the trend data to the proper arrays if the top node is set //add the trend data to the proper arrays if the top node is set
let cableTrendData = binTrend.cableTrend[cable.key()]; let cableTrendData = binTrend.cableTrend[cable.key()];
@ -1796,7 +1735,11 @@ export default function BinVisualizer(props: Props) {
<Typography variant="subtitle1" style={{ fontWeight: 800 }}> <Typography variant="subtitle1" style={{ fontWeight: 800 }}>
Bin Mode Bin Mode
</Typography> </Typography>
{modeChangeInProgress &&
<CircularProgress color="primary" size={25}/>
}
<ButtonGroup <ButtonGroup
disableAll={modeChangeInProgress}
buttons={[ buttons={[
{ {
title: "Storage", title: "Storage",
@ -1813,48 +1756,14 @@ export default function BinVisualizer(props: Props) {
{ {
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying", title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => { function: () => {
setShowInputMoisture(true) // setShowInputMoisture(true)
setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture
} }
} }
]} ]}
toggledButtons={determineToggle(mode)} toggledButtons={determineToggle(mode)}
toggle toggle
/> />
{/* <StyledToggleButtonGroup
id="tour-bin-mode"
value={mode}
exclusive
size="small"
aria-label="Bin Mode">
<StyledToggle
value={pond.BinMode.BIN_MODE_STORAGE}
aria-label="Storage Mode"
onClick={setModeStorage}>
Storage
</StyledToggle>
<StyledToggle
onClick={setModeCooldown}
value={pond.BinMode.BIN_MODE_COOLDOWN}
aria-label="Off">
Cooldown
</StyledToggle>
{bin.settings.inventory &&
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? (
<StyledToggle
onClick={() => setShowInputMoisture(true)}
value={pond.BinMode.BIN_MODE_HYDRATING}
aria-label="Hydrating Mode">
Hydrating
</StyledToggle>
) : (
<StyledToggle
onClick={() => setShowInputMoisture(true)}
value={pond.BinMode.BIN_MODE_DRYING}
aria-label="Drying Mode">
Drying
</StyledToggle>
)}
</StyledToggleButtonGroup> */}
</Box> </Box>
); );
}; };
@ -2037,7 +1946,7 @@ export default function BinVisualizer(props: Props) {
setOpenModeChange(true) setOpenModeChange(true)
}; };
const setModeDrying = () => { const setModeConditioning = () => {
if ( if (
bin.settings.inventory && bin.settings.inventory &&
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
@ -2051,82 +1960,82 @@ export default function BinVisualizer(props: Props) {
setOpenModeChange(true) setOpenModeChange(true)
}; };
const closeMoistureDialog = () => { // const closeMoistureDialog = () => {
// setNewPreset(0); // // setNewPreset(0);
setNewBinMode(bin.settings.mode); // setNewBinMode(bin.settings.mode);
setShowInputMoisture(false); // setShowInputMoisture(false);
}; // };
const moistureDialog = () => { // const moistureDialog = () => {
return ( // return (
<ResponsiveDialog open={showInputMoisture} onClose={closeMoistureDialog}> // <ResponsiveDialog open={showInputMoisture} onClose={closeMoistureDialog}>
<DialogTitle>Input new grain moisture</DialogTitle> // <DialogTitle>Input new grain moisture</DialogTitle>
<DialogContent> // <DialogContent>
<DialogContentText style={{ paddingBottom: theme.spacing(0) }}> // <DialogContentText style={{ paddingBottom: theme.spacing(0) }}>
Updating the current grain moisture will calibrate the estimate for more accurate // Updating the current grain moisture will calibrate the estimate for more accurate
results. The outdoor temperature will have some effect on the estimate as well; consider // results. The outdoor temperature will have some effect on the estimate as well; consider
updating with drastic changes in the weather or using a predicted average. // updating with drastic changes in the weather or using a predicted average.
</DialogContentText> // </DialogContentText>
<TextField // <TextField
label={"Grain Moisture"} // label={"Grain Moisture"}
value={moistureInput} // value={moistureInput}
onChange={event => setMoistureInput(event.target.value)} // onChange={event => setMoistureInput(event.target.value)}
InputProps={{ // InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment> // endAdornment: <InputAdornment position="end">%</InputAdornment>
}} // }}
style={{ marginTop: theme.spacing(2) }} // style={{ marginTop: theme.spacing(2) }}
fullWidth // fullWidth
variant="outlined" // variant="outlined"
/> // />
<Typography variant="body1" style={{ marginTop: theme.spacing(2) }}> // <Typography variant="body1" style={{ marginTop: theme.spacing(2) }}>
Weather // Weather
</Typography> // </Typography>
<TextField // <TextField
label={"Outdoor Temperature"} // label={"Outdoor Temperature"}
value={tempInput} // value={tempInput}
onChange={event => setTempInput(event.target.value)} // onChange={event => setTempInput(event.target.value)}
InputProps={{ // InputProps={{
endAdornment: ( // endAdornment: (
<InputAdornment position="end"> // <InputAdornment position="end">
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT // {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F" // ? "°F"
: "℃"} // : "℃"}
</InputAdornment> // </InputAdornment>
) // )
}} // }}
style={{ marginTop: theme.spacing(2) }} // style={{ marginTop: theme.spacing(2) }}
fullWidth // fullWidth
variant="outlined" // variant="outlined"
/> // />
<TextField // <TextField
label={"Outdoor Humidity"} // label={"Outdoor Humidity"}
value={outdoorHumidityInput} // value={outdoorHumidityInput}
onChange={event => setOutdoorHumidityInput(event.target.value)} // onChange={event => setOutdoorHumidityInput(event.target.value)}
InputProps={{ // InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment> // endAdornment: <InputAdornment position="end">%</InputAdornment>
}} // }}
style={{ marginTop: theme.spacing(2) }} // style={{ marginTop: theme.spacing(2) }}
fullWidth // fullWidth
variant="outlined" // variant="outlined"
/> // />
</DialogContent> // </DialogContent>
<DialogActions> // <DialogActions>
<Button // <Button
onClick={() => { // onClick={() => {
// setNewPreset(0); // // setNewPreset(0);
setNewBinMode(bin.settings.mode); // setNewBinMode(bin.settings.mode);
setShowInputMoisture(false); // setShowInputMoisture(false);
}} // }}
color="primary"> // color="primary">
Cancel // Cancel
</Button> // </Button>
<Button onClick={setModeDrying} color="primary"> // <Button onClick={setModeConditioning} color="primary">
Update // Update
</Button> // </Button>
</DialogActions> // </DialogActions>
</ResponsiveDialog> // </ResponsiveDialog>
); // );
}; // };
if (loading) { if (loading) {
return <Skeleton variant="rectangular" height={460} />; return <Skeleton variant="rectangular" height={460} />;
@ -2171,7 +2080,7 @@ export default function BinVisualizer(props: Props) {
return ( return (
<Card raised className={classes.cardContent}> <Card raised className={classes.cardContent}>
{moistureDialog()} {/* {moistureDialog()} */}
{cfmLowDialog()} {cfmLowDialog()}
{cfmHighDialog()} {cfmHighDialog()}
{changeGrain()} {changeGrain()}
@ -2201,11 +2110,15 @@ export default function BinVisualizer(props: Props) {
compDevMap={componentDevices} compDevMap={componentDevices}
preferences={preferences} preferences={preferences}
onClose={(refresh) => { onClose={(refresh) => {
setShowInputMoisture(false)
setOpenModeChange(false) setOpenModeChange(false)
if(refresh) updateBin(); if(refresh) updateBin();
}} }}
startChange={() => {
setModeChangeInProgress(true)
}}
changeComplete={() => {
setModeChangeInProgress(false)
}}
/> />
<Box paddingRight={1}> <Box paddingRight={1}>
{binMode()} {binMode()}

View file

@ -1,4 +1,4 @@
import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material"; import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { useComponentAPI, useInteractionsAPI } from "hooks"; import { useComponentAPI, useInteractionsAPI } from "hooks";
import { Component, Device, Interaction } from "models"; import { Component, Device, Interaction } from "models";
@ -21,6 +21,7 @@ import { lowerCase } from "lodash";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
import { getTemperatureUnit } from "utils"; import { getTemperatureUnit } from "utils";
import { GetGrainExtensionMap } from "grain"; import { GetGrainExtensionMap } from "grain";
import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress";
interface Props { interface Props {
open: boolean open: boolean
@ -33,6 +34,13 @@ interface Props {
binCables?: GrainCable[]; binCables?: GrainCable[];
compDevMap?: Map<string, number>; compDevMap?: Map<string, number>;
presets?: DevicePreset[]; presets?: DevicePreset[];
startChange: () => void;
changeComplete: () => void;
defaultTargetMoisture?: number;
changeTargetMoisture?: (newTarget: number) => {}
defaultOutdoorTemp?: number;
changeOutdoorTemp?: (newTemp: number) => {}
defaultOutdoorHumidity?: number;
} }
interface Option { interface Option {
@ -41,6 +49,11 @@ interface Option {
icon?: string; icon?: string;
} }
interface DialogStep {
label: string;
completed?: boolean;
}
export default function ModeChangeDialog(props: Props){ export default function ModeChangeDialog(props: Props){
const { const {
open, open,
@ -51,10 +64,13 @@ export default function ModeChangeDialog(props: Props){
preferences, preferences,
binCables, binCables,
compDevMap, compDevMap,
grain grain,
startChange,
changeComplete,
defaultTargetMoisture
} = props } = props
const [{as}] = useGlobalState() const [{as}] = useGlobalState()
// const [componentSets, setComponentSets] = useState<ComponentSet[]>([]) const [componentSets, setComponentSets] = useState<ComponentSet[]>([])
const interactionAPI = useInteractionsAPI() const interactionAPI = useInteractionsAPI()
const componentAPI = useComponentAPI(); const componentAPI = useComponentAPI();
const [selectedDevice, setSelectedDevice] = useState<Device | undefined>() const [selectedDevice, setSelectedDevice] = useState<Device | undefined>()
@ -73,6 +89,16 @@ export default function ModeChangeDialog(props: Props){
const grainExtensionMap = GetGrainExtensionMap(); const grainExtensionMap = GetGrainExtensionMap();
const [conflictingSetInteractions, setConflictingSetInteractions] = useState<Interaction[]>([]) const [conflictingSetInteractions, setConflictingSetInteractions] = useState<Interaction[]>([])
const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>() const [interactionsToAdd, setInteractionsToAdd] = useState<pond.MultiInteractionSettings>()
const [promiseStages, setPromiseStages] = useState<Stage[]>([])
const [dialogSteps, setDialogSteps] = useState<DialogStep[]>([])
const [currentStep, setCurrentStep] = useState(0)
//the state variables for the moisture
const [moistureInput, setMoistureInput] = useState<string>("")
useEffect(()=>{
setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0")
},[defaultTargetMoisture])
//get the interactions and components for the device when it is selected from the dropdown //get the interactions and components for the device when it is selected from the dropdown
useEffect(()=>{ useEffect(()=>{
@ -153,6 +179,39 @@ if (!selectedDevice) return;
setOptions(o); setOptions(o);
}, [devices, setOptions]); }, [devices, setOptions]);
//use effect here to determine the steps based on the bin mode
useEffect(() => {
let steps: DialogStep[] = []
switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE:
steps.push({
label: "Storage Settings"
})
break;
case pond.BinMode.BIN_MODE_COOLDOWN:
steps.push({
label: "Set Components"
})
steps.push({
label: "Progress"
})
break;
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
steps.push({
label: "Weather Conditions"
})
steps.push({
label: "Set Components"
})
steps.push({
label: "Progress"
})
}
setDialogSteps(steps)
},[binMode])
const buildHeaterInteraction = ( const buildHeaterInteraction = (
sensor: Plenum | Ambient, sensor: Plenum | Ambient,
heater: Controller heater: Controller
@ -235,7 +294,8 @@ if (!selectedDevice) return;
}); });
conditions.push(tempCondition); conditions.push(tempCondition);
interaction.conditions = conditions; interaction.conditions = conditions;
//the the heater output to auto so that when the components are update it uses the new mode
heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
return interaction; return interaction;
}; };
@ -333,40 +393,72 @@ if (!selectedDevice) return;
conditions.push(fanConditionTwo); conditions.push(fanConditionTwo);
interaction.conditions = conditions; interaction.conditions = conditions;
//set the output mode to auto in the fan so that when the components are updated it uses the new mode
fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO
return interaction; return interaction;
}; };
//this is what will use the conflicting interactions //this is what will use the conflicting interactions
const execute = () => { const buildPromises = () => {
if(!selectedDevice || !interactionsToAdd) return //if there is no device selected or there are no interactions to add, do nothing //if there is no device selected or there are no interactions to add, do nothing
//create an array of promises to delete the conflicting interactions if(!selectedDevice || !interactionsToAdd) return
let conflictingInteractionsPromise = conflictingSetInteractions.map(i => {
return interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as); //build the stages to pass into the PromiseProgress
//stage one is to remove conflicting interactions
let stages: Stage[] = []
let stage1: Stage = {
title: "Remove Conflicting Interactions",
steps: []
}
conflictingSetInteractions.forEach(i => {
let newStep: PromiseStep = {
title: "Removing Interaction",
promise: interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as)
}
stage1.steps.push(newStep);
}); });
Promise.all([...conflictingInteractionsPromise]).then(() => { stages.push(stage1)
//the conflicting interactions were removed now add the new ones //stage two is to add the new interactions
interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) let stage2: Stage = {
}).catch(err => { title: "Add New Interactions",
//there was a problem removing any conflicting interactions steps: [
{
title: "Adding Interactions",
promise: interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd)
}
]
}
stages.push(stage2)
//stage three is to update the controller components
let stage3: Stage = {
title: "Update Controllers",
steps: []
}
componentSets.forEach(set => {
set.controllers.forEach(controller => {
console.log(controller)
let newStep: PromiseStep = {
title: "Updating " + controller.name(),
promise: componentAPI.update(selectedDevice.id(), controller.settings)
}
stage3.steps.push(newStep)
}) })
})
stages.push(stage3)
//set those stages to a state variable
setPromiseStages(stages)
} }
/** //this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them
* this function takes in the sets that are passed in and returns the promises needed for removing conflicting interactions and adding the new ones
* IT DOES NOT EXECUTE THEM ONLY CREATES THE PROMISES
* returns an object containing two values,
* one is an array of promises for conflicting interactions to remove and the other is the promise for adding the new interactions
* @param sets
* @returns
*/
const createInteractions = (sets: ComponentSet[]) => { const createInteractions = (sets: ComponentSet[]) => {
if(!selectedDevice) return undefined //if there is no device that was selected, do nothing if(!selectedDevice) return undefined //if there is no device that was selected, do nothing
let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create()
let conflictingInteractions: Interaction[] = [] let conflictingInteractions: Interaction[] = []
let linkedComponents = deviceComponents.get(selectedDevice.id().toString()); let linkedComponents = deviceComponents.get(selectedDevice.id().toString());
//loop through the sets //loop through the sets
sets.forEach(async (set) => { sets.forEach((set) => {
//loop through the controllers //loop through the controllers
set.controllers.forEach(controller => { set.controllers.forEach(controller => {
//filter the conflicting interactions for that controller //filter the conflicting interactions for that controller
@ -419,6 +511,7 @@ if (!selectedDevice) return;
} }
}) })
}) })
setComponentSets([...sets])
return { return {
conflicting: conflictingInteractions, conflicting: conflictingInteractions,
toAdd: multiInteractionSettings toAdd: multiInteractionSettings
@ -460,16 +553,73 @@ if (!selectedDevice) return;
const close = (refresh: boolean) => { const close = (refresh: boolean) => {
//clear state data
setCurrentStep(0)
setInteractionsToAdd(undefined)
setPromiseStages([])
onClose(refresh) onClose(refresh)
} }
const content = () => { //this step will only be used for drying and hydrating
const moistureStep = () => {
return (
<Box>
<DialogContentText style={{ paddingBottom: theme.spacing(0) }}>
Updating the current grain moisture will calibrate the estimate for more accurate
results. The outdoor temperature will have some effect on the estimate as well; consider
updating with drastic changes in the weather or using a predicted average.
</DialogContentText>
<TextField
label={"Grain Moisture"}
value={moistureInput}
onChange={event => setMoistureInput(event.target.value)}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
<Typography variant="body1" style={{ marginTop: theme.spacing(2) }}>
Weather
</Typography>
<TextField
label={"Outdoor Temperature"}
value={tempInput}
onChange={event => setTempInput(event.target.value)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT
? "°F"
: "℃"}
</InputAdornment>
)
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
<TextField
label={"Outdoor Humidity"}
value={outdoorHumidityInput}
onChange={event => setOutdoorHumidityInput(event.target.value)}
InputProps={{
endAdornment: <InputAdornment position="end">%</InputAdornment>
}}
style={{ marginTop: theme.spacing(2) }}
fullWidth
variant="outlined"
/>
</Box>
)
};
const selectDeviceStep = () => {
switch(binMode){ switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE: case pond.BinMode.BIN_MODE_STORAGE:
return ( return (
<React.Fragment> <Box>
<DialogTitle>Entering Storage Mode</DialogTitle>
<DialogContent>
{binCables && binCables?.length > 0 && compDevMap && ( {binCables && binCables?.length > 0 && compDevMap && (
<Box marginBottom={2}> <Box marginBottom={2}>
<CableTopNodeSummary <CableTopNodeSummary
@ -484,33 +634,13 @@ if (!selectedDevice) return;
left alone. Heaters/Fans will be turned off. left alone. Heaters/Fans will be turned off.
</DialogContentText> </DialogContentText>
{deviceSelector()} {deviceSelector()}
</DialogContent> </Box>
<DialogActions>
<Button variant="contained" onClick={()=>{
close(false)
}}>
Close
</Button>
{/* Confirm - button to enter storage and end conditioning */}
<Button variant="contained" color="primary" onClick={() => {
close(true)
if (selectedDevice) {
//update the controllers to be off for the selected device
// updateControllers();
}
}}>
Confirm
</Button>
</DialogActions>
</React.Fragment>
) )
case pond.BinMode.BIN_MODE_COOLDOWN:
case pond.BinMode.BIN_MODE_DRYING: case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_COOLDOWN:
case pond.BinMode.BIN_MODE_HYDRATING: case pond.BinMode.BIN_MODE_HYDRATING:
return ( return (
<React.Fragment> <Box>
<DialogTitle>Choose Device for {modeDescription()} Method</DialogTitle>
<DialogContent>
<DialogContentText> <DialogContentText>
{modeDescription()} control is handled by interactions between multiple components on the {modeDescription()} control is handled by interactions between multiple components on the
same device. Please choose which device and components will handle this bin's{" "} same device. Please choose which device and components will handle this bin's{" "}
@ -528,34 +658,145 @@ if (!selectedDevice) return;
//then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones //then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones
let i = createInteractions(sets) let i = createInteractions(sets)
if(i !== undefined){ if(i !== undefined){
console.log(i)
setConflictingSetInteractions(i.conflicting) setConflictingSetInteractions(i.conflicting)
setInteractionsToAdd(i.toAdd) setInteractionsToAdd(i.toAdd)
} }
}}/> }}
</DialogContent> />
<DialogActions> </Box>
<Button variant="contained" onClick={()=>{
close(false)
}}>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={()=>{
//add new interactions to the component sets
execute()
close(true)
}}>
Submit
</Button>
</DialogActions>
</React.Fragment>
) )
} }
} }
const progressStep = () => {
return ( return (
<Box>
<PromiseProgress
stages={promiseStages}
failFast
onComplete={()=>{
console.log("COMPLETED PROMISES")
setCurrentStep(0)//reset the stepper
changeComplete()//change the boolean preventing more mode changes
}}
/>
</Box>
)
}
const stepper = () => {
return (
<Stepper nonLinear activeStep={currentStep} style={{ width: "100%" }}>
{dialogSteps.map(dialogStep => {
const labelProps: {
optional?: React.ReactNode;
} = {};
return (
<Step key={dialogStep.label} completed={dialogStep.completed}>
<StepLabel {...labelProps}>{dialogStep.label}</StepLabel>
</Step>
);
})}
</Stepper>
);
}
const actions = () => {
let buttons: JSX.Element[] = []
//this button should be on every step of all of them
buttons.push(
<Button onClick={() => {close(false)}}>Close</Button>
)
// this button should appear if the step they are on is not 0 and not the last step
if(currentStep !== 0 && currentStep !== dialogSteps.length-1){
buttons.push(
<Button onClick={()=>{setCurrentStep(currentStep-1)}}>Back</Button>
)
}
// this button should appear if the step they are on is less than the last step -1
if(currentStep < dialogSteps.length-2){
buttons.push(
<Button onClick={() => {setCurrentStep(currentStep+1)}}>Next</Button>
)
}
if((binMode === pond.BinMode.BIN_MODE_STORAGE || binMode === pond.BinMode.BIN_MODE_COOLDOWN && currentStep === 0) ||
binMode === pond.BinMode.BIN_MODE_DRYING || binMode === pond.BinMode.BIN_MODE_HYDRATING && currentStep === 1){
buttons.push(
<Button onClick={() => {
//if the bin mode is storage, just update the controllers for the selected device and close
if(binMode === pond.BinMode.BIN_MODE_STORAGE){
close(true)
if (selectedDevice) {
//update the controllers to be off for the selected device
heaters.forEach(heater => {
componentAPI
.update(selectedDevice.id(), heater.settings, undefined, undefined, as)
.then()
.catch();
});
fans.forEach(fan => {
componentAPI
.update(selectedDevice.id(), fan.settings, undefined, undefined, as)
.then()
.catch();
});
}
}else{
setCurrentStep(dialogSteps.length-1)//set the stepper to the progress step
buildPromises()
}
//if it is cooldown, drying, or hydrating, it will need to go to the progress step and initiate all of the promises
}}>Confirm</Button>
)
}
return buttons
}
const stepperContent = (step: number) => {
switch(binMode){
case pond.BinMode.BIN_MODE_STORAGE:
return selectDeviceStep();
case pond.BinMode.BIN_MODE_COOLDOWN:
switch (step) {
case 1:
return progressStep();
default:
return selectDeviceStep();
}
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
switch (step) {
case 1:
return selectDeviceStep();
case 2:
return progressStep();
default:
return moistureStep();
}
}
};
return (
<React.Fragment>
{/* <PromiseProgress
open={openProgress}
stages={promiseStages}
onClose={()=>{
setOpenProgress(false)
close(refresh)
}} /> */}
<ResponsiveDialog open={open} onClose={() => {close(false)}}> <ResponsiveDialog open={open} onClose={() => {close(false)}}>
{content()} <DialogTitle>
{binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"}
</DialogTitle>
<DialogContent>
{stepper()}
{stepperContent(currentStep)}
</DialogContent>
<DialogActions>
{actions()}
</DialogActions>
</ResponsiveDialog> </ResponsiveDialog>
</React.Fragment>
) )
} }

View file

@ -19,6 +19,10 @@ export interface ButtonData {
* The function to run when the button is clicked or toggled on when toggle is true * The function to run when the button is clicked or toggled on when toggle is true
*/ */
function: () => void function: () => void
/**
* whether the button is disabled or not
*/
disabled?: boolean
} }
interface Props { interface Props {
@ -49,6 +53,10 @@ interface Props {
* Numerical value for the size of the text for buttons that dont use the icon * Numerical value for the size of the text for buttons that dont use the icon
*/ */
textSize?: number textSize?: number
/**
* When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data
*/
disableAll?: boolean
} }
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
@ -88,7 +96,7 @@ const useStyles = makeStyles((theme: Theme) => {
}) })
export default function ButtonGroup(props: Props){ export default function ButtonGroup(props: Props){
const { buttons, toggle, toggledButtons, multiToggle, textSize } = props const { buttons, toggle, toggledButtons, multiToggle, textSize, disableAll } = props
const classes = useStyles() const classes = useStyles()
const [currentToggle, setCurrentToggle] = useState<number[]>([]) const [currentToggle, setCurrentToggle] = useState<number[]>([])
@ -135,6 +143,7 @@ export default function ButtonGroup(props: Props){
<Box className={classes.container} display="flex" flexDirection="row"> <Box className={classes.container} display="flex" flexDirection="row">
{buttons.map((b, i) => ( {buttons.map((b, i) => (
<Button <Button
disabled={disableAll ?? b.disabled}
key={i} key={i}
className={checkToggled(i) ? classes.buttonToggled : classes.button} className={checkToggled(i) ? classes.buttonToggled : classes.button}
onClick={() => { onClick={() => {

View file

@ -0,0 +1,128 @@
import { AxiosResponse } from "axios"
import ResponsiveDialog from "./ResponsiveDialog"
import { Box, Button, CircularProgress, DialogActions, DialogContent, DialogTitle, Icon, Typography } from "@mui/material"
import { useEffect, useState } from "react"
import { CheckCircle, Error, Pending } from "@mui/icons-material"
import React from "react"
export interface Stage {
title: string
steps: Step[]
}
export interface Step {
title: string
promise: Promise<AxiosResponse>
onComplete?: () => void
onError?: () => void
}
interface Props {
stages: Stage[]
failFast?: boolean
onComplete?: () => void
}
enum progress {
Waiting = 1,
InProgress = 2,
Complete = 3,
Failed = 4
}
export function PromiseProgress(props: Props){
const {stages, failFast, onComplete} = props
const [completion, setCompletion] = useState<Map<string, progress>>(new Map())
const execute = () => {
let completionMap: Map<string, progress> = new Map()
stages.forEach((stage, i) => {
stage.steps.forEach((_, k) => {
completionMap.set(i + "-" + k, progress.Waiting)
})
})
setCompletion(completionMap)
const runStages = async () => {
let progMap = new Map(completionMap)
for (const [i, stage] of stages.entries()) {
// For each stage, map the steps into their own promise chains
const stepPromises = stage.steps.map((step, k) => {
// mark step as in progress
progMap.set(i+"-"+k, progress.InProgress);
setCompletion(new Map(progMap));
return step.promise
.then(resp => {
// mark step as completed
progMap.set(i+"-"+k, progress.Complete);
setCompletion(new Map(progMap));
if (step.onComplete) step.onComplete();
})
.catch(err => {
// mark step as failed
progMap.set(i+"-"+k, progress.Failed);
setCompletion(new Map(progMap));
if(failFast){
throw err; // this rejects this step promise
}
})
});
// Wait for all steps in the current stage to finish
// (they run in parallel)
await Promise.all(stepPromises);
}
};
runStages().catch(err => {
console.error("Execution stopped due to error:", err);
}).finally(() => {
if(onComplete) onComplete()
});
}
const getCompletion = (completion?: progress) => {
switch(completion){
case progress.InProgress:
return <CircularProgress />
case progress.Complete:
return <CheckCircle />
case progress.Failed:
return <Error />
default://default will run if it is waiting or there was no completion
return <Pending />
}
}
const displayStage = (stage: Stage, number: number) => {
return (
<Box key={number}>
<Typography>Stage {stage.title}</Typography>
{stage.steps.map((step, i) => (
<Box key={number+"-"+i} display="flex" flexDirection="row" justifyContent="space-between" margin={2} alignContent="center" alignItems="center">
<Typography>{step.title}</Typography>
{getCompletion(completion.get(number + "-" + i))}
</Box>
))}
</Box>
)
}
return (
<React.Fragment>
<DialogTitle>
Progress
</DialogTitle>
<DialogContent>
{stages.map((stage, number) => displayStage(stage, number))}
</DialogContent>
<DialogActions>
<Button onClick={() => {
execute()
}}>
Start
</Button>
</DialogActions>
</React.Fragment>
)
}