524 lines
16 KiB
TypeScript
524 lines
16 KiB
TypeScript
import {
|
|
Button,
|
|
Checkbox,
|
|
darken,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Divider,
|
|
Grid2,
|
|
lighten,
|
|
List,
|
|
ListItem,
|
|
ListItemSecondaryAction,
|
|
ListItemText,
|
|
Paper,
|
|
Step,
|
|
StepLabel,
|
|
Stepper,
|
|
Tab,
|
|
Tabs,
|
|
TextField,
|
|
Theme,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { makeStyles } from "@mui/styles";
|
|
import CancelSubmit from "common/CancelSubmit";
|
|
import DeleteButton from "common/DeleteButton";
|
|
import Loader from "common/Loader";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import SearchBar from "common/SearchBar";
|
|
import { useDeviceAPI, useGroupAPI, usePrevious, useSnackbar } from "hooks";
|
|
import { Device, Group } from "models";
|
|
import { filterDevices } from "pbHelpers/Device";
|
|
import { groupsAreEqual } from "pbHelpers/Group";
|
|
import { useGlobalState } from "providers";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import RemoveGroup from "./RemoveGroup";
|
|
|
|
const useStyles = makeStyles((theme: Theme) => {
|
|
return ({
|
|
deviceListContainer: {
|
|
marginBottom: theme.spacing(1)
|
|
},
|
|
devicesList: {
|
|
overflow: "auto",
|
|
minHeight: "25vh",
|
|
maxHeight: "35vh",
|
|
height: "auto",
|
|
background:
|
|
theme.palette.mode === "dark"
|
|
? lighten(theme.palette.background.paper, 0.04)
|
|
: darken(theme.palette.background.paper, 0.04)
|
|
}
|
|
});
|
|
});
|
|
|
|
interface Props {
|
|
initialGroup?: Group;
|
|
mode?: "add" | "update" | "remove" | undefined;
|
|
isDialogOpen: boolean;
|
|
closeDialogCallback: Function;
|
|
refreshCallback: () => void;
|
|
canEdit?: boolean;
|
|
groupDevices?: Device[];
|
|
removeGroupCallback?: Function;
|
|
}
|
|
|
|
export default function GroupSettings(props: Props) {
|
|
const classes = useStyles();
|
|
const { success, error, warning } = useSnackbar();
|
|
const {
|
|
initialGroup,
|
|
mode,
|
|
isDialogOpen,
|
|
closeDialogCallback,
|
|
refreshCallback,
|
|
canEdit,
|
|
groupDevices,
|
|
removeGroupCallback,
|
|
} = props;
|
|
const prevInitialGroup = usePrevious(initialGroup);
|
|
const [{as}] = useGlobalState();
|
|
const groupAPI = useGroupAPI();
|
|
const deviceAPI = useDeviceAPI();
|
|
const snackbar = useSnackbar()
|
|
const [devices, setDevices] = useState<Device[]>([]);
|
|
const [group, setGroup] = useState<Group>(initialGroup ? Group.clone(initialGroup) : new Group());
|
|
const [isRemoveGroupOpen, setIsRemoveGroupOpen] = useState<boolean>(
|
|
mode === "remove" ? true : false
|
|
);
|
|
const [tabIndex, setTabIndex] = useState<number>(0);
|
|
const prevTabIndex = usePrevious(tabIndex);
|
|
const [steps] = useState<string[]>(["General group information", "Select devices for the group"]);
|
|
const [deviceSearch, setDeviceSearch] = useState<string>("");
|
|
const [loadingDevices, setLoadingDevices] = useState<boolean>(false);
|
|
|
|
const [groupDeviceNumbers, setGroupDeviceNumbers] = useState<number[]>([])
|
|
|
|
// Use a ref to store removeGroupCallback
|
|
const removeCallbackRef = React.useRef(removeGroupCallback);
|
|
|
|
// Update ref when prop changes
|
|
useEffect(() => {
|
|
console.log("Updating removeCallbackRef with:", removeGroupCallback);
|
|
if (removeGroupCallback) removeCallbackRef.current = removeGroupCallback;
|
|
}, [removeGroupCallback]);
|
|
|
|
useEffect(() => {
|
|
let newNumbers: number[] = []
|
|
groupDevices?.forEach(device => {
|
|
newNumbers.push(device.id())
|
|
})
|
|
setGroupDeviceNumbers(newNumbers)
|
|
}, [groupDevices])
|
|
|
|
const loadDevices = useCallback(() => {
|
|
setLoadingDevices(true);
|
|
deviceAPI
|
|
.list(1000000, 0, "asc", undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,undefined,as)
|
|
.then((response: any) => {
|
|
let rDevices: Device[] = response.data.devices
|
|
? response.data.devices.map((device: any) => Device.any(device))
|
|
: [];
|
|
|
|
setDevices(rDevices);
|
|
})
|
|
.catch((_error: any) => {
|
|
setDevices([]);
|
|
})
|
|
.finally(() => setLoadingDevices(false));
|
|
}, [deviceAPI, as]);
|
|
|
|
useEffect(() => {
|
|
if (prevInitialGroup !== initialGroup) {
|
|
setGroup(initialGroup ? Group.clone(initialGroup) : new Group());
|
|
//setDevices([]);
|
|
}
|
|
|
|
if (prevTabIndex !== 1 && tabIndex === 1) {
|
|
loadDevices();
|
|
}
|
|
}, [initialGroup, loadDevices, prevInitialGroup, prevTabIndex, props, tabIndex]);
|
|
|
|
const close = () => {
|
|
closeDialogCallback();
|
|
setGroup(initialGroup ? Group.clone(initialGroup) : new Group());
|
|
setDevices([]);
|
|
setTabIndex(0);
|
|
};
|
|
|
|
const submit = () => {
|
|
const groupName = group.name();
|
|
switch (mode) {
|
|
case "add":
|
|
groupAPI
|
|
.addGroup(group.settings)
|
|
.then((_response: any) => {
|
|
success(groupName + " was successfully created");
|
|
close();
|
|
refreshCallback();
|
|
})
|
|
.catch((err: any) => {
|
|
err.response.data.error
|
|
? warning(err.response.data.error)
|
|
: error("Error occured when creating a group");
|
|
close();
|
|
})
|
|
.finally(() => setTabIndex(0));
|
|
break;
|
|
default:
|
|
groupAPI
|
|
.updateGroup(group.id(), group.settings)
|
|
.then((_response: any) => {
|
|
success(groupName + " was successfully updated");
|
|
close();
|
|
refreshCallback();
|
|
})
|
|
.catch((err: any) => {
|
|
err.response.data.error
|
|
? warning(err.response.data.error)
|
|
: error("Error occured when updating " + groupName);
|
|
close();
|
|
});
|
|
break;
|
|
}
|
|
};
|
|
|
|
const openRemoveGroup = () => {
|
|
setIsRemoveGroupOpen(true);
|
|
};
|
|
|
|
const closeRemoveGroup = () => {
|
|
setIsRemoveGroupOpen(false);
|
|
if (mode === "remove") {
|
|
close();
|
|
}
|
|
};
|
|
|
|
const isFormValid = () => {
|
|
const validGroup: boolean = group !== undefined && group !== null;
|
|
const validUpdate: boolean =
|
|
mode !== "update" || !groupsAreEqual(Group.clone(initialGroup), group);
|
|
|
|
return validGroup && validUpdate;
|
|
};
|
|
|
|
const changeTab = (value: number) => {
|
|
setTabIndex(value);
|
|
};
|
|
|
|
const nextStep = () => {
|
|
let nextStepIndex = tabIndex + 1;
|
|
setTabIndex(nextStepIndex);
|
|
};
|
|
|
|
const backStep = () => {
|
|
let backStepIndex = tabIndex - 1;
|
|
setTabIndex(backStepIndex);
|
|
};
|
|
|
|
const changeDeviceSearch = (value: string) => {
|
|
setDeviceSearch(value);
|
|
};
|
|
|
|
const changeName = (event: any) => {
|
|
let updatedGroup = Group.clone(group);
|
|
updatedGroup.settings.name = event.target.value;
|
|
setGroup(updatedGroup);
|
|
};
|
|
|
|
const changeDescription = (event: any) => {
|
|
let updatedGroup = Group.clone(group);
|
|
updatedGroup.settings.description = event.target.value;
|
|
setGroup(updatedGroup);
|
|
};
|
|
|
|
const changeDevices = (device: number) => {
|
|
let updatedGroup = Group.clone(group);
|
|
const exists = updatedGroup.settings.devices.includes(device);
|
|
if (exists) {
|
|
updatedGroup.settings.devices = updatedGroup.settings.devices.filter(
|
|
groupDevice => groupDevice !== device
|
|
);
|
|
} else {
|
|
updatedGroup.settings.devices.push(device);
|
|
}
|
|
setGroup(updatedGroup);
|
|
};
|
|
|
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
const title = () => {
|
|
switch (mode) {
|
|
case "add":
|
|
return (
|
|
<React.Fragment>
|
|
Group Creation
|
|
<Typography variant="body2" color="textSecondary" gutterBottom>
|
|
Groups are a great way to organize your devices
|
|
</Typography>
|
|
</React.Fragment>
|
|
);
|
|
case "update":
|
|
return (
|
|
<React.Fragment>
|
|
Group Settings
|
|
<Typography variant="body2" color="textSecondary">
|
|
{group.name()}
|
|
</Typography>
|
|
</React.Fragment>
|
|
);
|
|
default:
|
|
return <React.Fragment />;
|
|
}
|
|
};
|
|
|
|
const generalGroupContent = () => {
|
|
return (
|
|
<React.Fragment>
|
|
<TextField
|
|
id="group-name"
|
|
label="Name"
|
|
value={group.settings.name}
|
|
onChange={changeName}
|
|
margin="normal"
|
|
variant="outlined"
|
|
disabled={!canEdit}
|
|
fullWidth
|
|
/>
|
|
|
|
<TextField
|
|
id="group-description"
|
|
label="Description"
|
|
multiline
|
|
rows={4}
|
|
// rowsMax="4"
|
|
value={group.settings.description}
|
|
onChange={changeDescription}
|
|
margin="normal"
|
|
variant="outlined"
|
|
disabled={!canEdit}
|
|
fullWidth
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
const devicesGroupContent = () => {
|
|
if (mode !== "add") return devicesTab();
|
|
return (
|
|
<Paper elevation={0} className={classes.deviceListContainer}>
|
|
<SearchBar value={deviceSearch} onChange={changeDeviceSearch} shape="square" />
|
|
{loadingDevices ? (
|
|
<List className={classes.devicesList}>
|
|
<ListItem>
|
|
<Grid2 container justifyContent="center">
|
|
<Loader size="small" />
|
|
</Grid2>
|
|
</ListItem>
|
|
</List>
|
|
) : (
|
|
<List dense className={classes.devicesList}>
|
|
{filterDevices(deviceSearch, devices, [])
|
|
.sort((a, b: Device) => a.name().localeCompare(b.name()))
|
|
.map(device => {
|
|
const label = `checkbox-list-secondary-label-${device.id()}`;
|
|
return (
|
|
<ListItem key={device.id()} onClick={() => changeDevices(device.id())}>
|
|
<ListItemText id={label} primary={device.name()} />
|
|
<ListItemSecondaryAction>
|
|
<Checkbox
|
|
edge="end"
|
|
onChange={() => changeDevices(device.id())}
|
|
checked={groupDeviceNumbers.includes(device.id())}
|
|
inputProps={{ "aria-labelledby": label }}
|
|
disabled={!canEdit}
|
|
/>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
);
|
|
})}
|
|
</List>
|
|
)}
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
//useEffect(() => {
|
|
// if (tabIndex === 1) loadDevices()
|
|
//}, [groupDevices])
|
|
|
|
const addDevice = (device: number) => {
|
|
groupAPI.addDevice(group.id(), device).then(() => {
|
|
let newDevices = [...groupDeviceNumbers];
|
|
newDevices.push(device)
|
|
setGroupDeviceNumbers(newDevices)
|
|
snackbar.success("Device " + device + " successfully added to group")
|
|
refreshCallback();
|
|
}).catch(() => {
|
|
snackbar.error("Failed to add device")
|
|
});
|
|
};
|
|
|
|
const removeDevice = (device: number) => {
|
|
groupAPI.removeDevice(group.id(), device).then(() => {
|
|
let newDevices = groupDeviceNumbers.filter(dev => dev !== device)
|
|
setGroupDeviceNumbers(newDevices)
|
|
snackbar.success("Device " + device + " successfully removed from group")
|
|
refreshCallback();
|
|
}).catch(() => {
|
|
snackbar.error("Failed to remove device")
|
|
})
|
|
};
|
|
|
|
const devicesTab = () => {
|
|
return (
|
|
<Paper elevation={0} className={classes.deviceListContainer}>
|
|
<SearchBar value={deviceSearch} onChange={changeDeviceSearch} shape="square" />
|
|
{loadingDevices ? (
|
|
<List className={classes.devicesList}>
|
|
<ListItem>
|
|
<Grid2 container justifyContent="center">
|
|
<Loader size="small" />
|
|
</Grid2>
|
|
</ListItem>
|
|
</List>
|
|
) : (
|
|
<List dense className={classes.devicesList}>
|
|
{filterDevices(deviceSearch, devices, [])
|
|
.sort((a, b: Device) => a.name().localeCompare(b.name()))
|
|
.map(device => {
|
|
const label = `checkbox-list-secondary-label-${device.id()}`;
|
|
return (
|
|
<ListItem key={device.id()} onClick={() => changeDevices(device.id())}>
|
|
<ListItemText id={label} primary={device.name()} />
|
|
<ListItemSecondaryAction>
|
|
<Checkbox
|
|
edge="end"
|
|
onChange={(_, checked) => {
|
|
if (checked) addDevice(device.id());
|
|
else removeDevice(device.id());
|
|
}}
|
|
// checked={Boolean(groupDevices?.find(dev => dev.id() === device.id()))}
|
|
checked={groupDeviceNumbers.includes(device.id())}
|
|
inputProps={{ "aria-labelledby": label }}
|
|
disabled={!canEdit}
|
|
/>
|
|
</ListItemSecondaryAction>
|
|
</ListItem>
|
|
);
|
|
})}
|
|
</List>
|
|
)}
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
const content = () => {
|
|
if (mode === "add") {
|
|
return (
|
|
<React.Fragment>
|
|
{tabIndex === 0 && generalGroupContent()}
|
|
{tabIndex === 1 && devicesGroupContent()}
|
|
<Stepper activeStep={tabIndex} alternativeLabel>
|
|
{steps.map(label => (
|
|
<Step key={label}>
|
|
<StepLabel>{label}</StepLabel>
|
|
</Step>
|
|
))}
|
|
</Stepper>
|
|
<Divider />
|
|
</React.Fragment>
|
|
);
|
|
} else if (mode === "update") {
|
|
return (
|
|
<React.Fragment>
|
|
{tabIndex === 0 && generalGroupContent()}
|
|
{tabIndex === 1 && devicesGroupContent()}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
return <React.Fragment />;
|
|
};
|
|
|
|
const actions = () => {
|
|
return (
|
|
<Grid2 container justifyContent="space-between" direction="row" width={"100%"}>
|
|
<Grid2 size={{ xs: 5 }}>
|
|
{mode === "add" ? (
|
|
<Button onClick={backStep} color="primary" disabled={tabIndex === 0}>
|
|
Back
|
|
</Button>
|
|
) : (
|
|
mode === "update" &&
|
|
canEdit && <DeleteButton onClick={openRemoveGroup}>Delete</DeleteButton>
|
|
)}
|
|
</Grid2>
|
|
<Grid2 size={{ xs: 7 }} container justifyContent="flex-end">
|
|
<CancelSubmit
|
|
onSubmit={(mode === "add" && tabIndex === steps.length - 1) || mode === "update" ? submit : nextStep}
|
|
submitText={(mode === "add" && tabIndex === steps.length - 1) || mode === "update" ? "Submit" : "Next"}
|
|
onCancel={close}
|
|
submitDisabled={!isFormValid()}/>
|
|
{/* <Button onClick={close} color="primary">
|
|
Cancel
|
|
</Button>
|
|
{(mode === "add" && tabIndex === steps.length - 1) || mode === "update"
|
|
? canEdit && (
|
|
<Button onClick={submit} color="primary" disabled={!isFormValid()}>
|
|
Submit
|
|
</Button>
|
|
)
|
|
: mode === "add" && (
|
|
<Button onClick={nextStep} color="primary">
|
|
Next
|
|
</Button>
|
|
)} */}
|
|
</Grid2>
|
|
</Grid2>
|
|
);
|
|
};
|
|
|
|
const dialogs = () => {
|
|
console.log("dialogs: removeCallbackRef.current =", removeCallbackRef.current); // Log ref
|
|
console.log("dialogs: removeGroupCallback =", removeGroupCallback); // Log prop
|
|
return (
|
|
<RemoveGroup
|
|
group={group}
|
|
isDialogOpen={isRemoveGroupOpen}
|
|
closeDialogCallback={closeRemoveGroup}
|
|
removeCallback={removeCallbackRef.current}
|
|
/>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ResponsiveDialog
|
|
open={isDialogOpen}
|
|
onClose={close}
|
|
aria-labelledby="group-settings-dialog"
|
|
maxWidth="sm"
|
|
fullWidth>
|
|
<DialogTitle id="group-settings-title">{title()}</DialogTitle>
|
|
{mode === "update" && (
|
|
<Tabs
|
|
value={tabIndex}
|
|
onChange={(_event, value) => {
|
|
changeTab(value);
|
|
}}
|
|
indicatorColor="secondary"
|
|
textColor="inherit"
|
|
variant="fullWidth">
|
|
<Tab label="General" />
|
|
<Tab label="Devices" />
|
|
</Tabs>
|
|
)}
|
|
<Divider />
|
|
<DialogContent>{content()}</DialogContent>
|
|
<DialogActions>{actions()}</DialogActions>
|
|
{dialogs()}
|
|
</ResponsiveDialog>
|
|
);
|
|
}
|