groupd now generate tabs

This commit is contained in:
Carter 2024-12-19 14:10:16 -06:00
parent c1ff72025a
commit 916d0f9132
4 changed files with 609 additions and 8 deletions

484
src/group/GroupSettings.tsx Normal file
View file

@ -0,0 +1,484 @@
import {
Button,
Checkbox,
darken,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Grid,
Grid2,
lighten,
List,
ListItem,
ListItemSecondaryAction,
ListItemText,
Paper,
Step,
StepLabel,
Stepper,
Tab,
Tabs,
TextField,
Theme,
Typography
} from "@mui/material";
import { makeStyles } from "@mui/styles";
// import { darken, lighten } from "@material-ui/core/styles/colorManipulator";
// import { Theme } from "@material-ui/core/styles/createMuiTheme";
import DeleteButton from "common/DeleteButton";
import Loader from "common/Loader";
import ResponsiveDialog from "common/ResponsiveDialog";
import SearchBar from "common/SearchBar";
// import SearchBar from "common/SearchBar";
// import RemoveGroup from "group/RemoveGroup";
import { useDeviceAPI, useGroupAPI, usePrevious, useSnackbar } from "hooks";
import { Device, Group } from "models";
import { filterDevices } from "pbHelpers/Device";
import { groupsAreEqual } from "pbHelpers/Group";
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: Function;
canEdit?: boolean;
groupDevices?: Device[];
}
export default function GroupSettings(props: Props) {
const classes = useStyles();
const { success, error, warning } = useSnackbar();
const {
initialGroup,
mode,
isDialogOpen,
closeDialogCallback,
refreshCallback,
canEdit,
groupDevices
} = props;
const prevInitialGroup = usePrevious(initialGroup);
const groupAPI = useGroupAPI();
const deviceAPI = useDeviceAPI();
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 loadDevices = useCallback(() => {
setLoadingDevices(true);
deviceAPI
.list(1000000, 0, "asc")
.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]);
useEffect(() => {
if (prevInitialGroup !== initialGroup) {
setGroup(initialGroup ? Group.clone(initialGroup) : new Group());
//setDevices([]);
}
if (prevTabIndex !== 1 && tabIndex === 1) {
loadDevices();
}
}, [initialGroup, loadDevices, prevInitialGroup, prevTabIndex, props, tabIndex, groupDevices]);
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={group.settings.devices.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(() => {
refreshCallback();
});
};
const removeDevice = (device: number) => {
groupAPI.removeDevice(group.id(), device).then(() => {
refreshCallback();
});
};
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()))}
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">
<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">
<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 = () => {
return (
<RemoveGroup
group={group}
isDialogOpen={isRemoveGroupOpen}
closeDialogCallback={closeRemoveGroup}
/>
);
};
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>
);
}

84
src/group/RemoveGroup.tsx Normal file
View file

@ -0,0 +1,84 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Theme,
Typography
} from "@mui/material";
import { makeStyles } from "@mui/styles";
import { useGroupAPI, useSnackbar } from "hooks";
import { Group } from "models";
import { useNavigate } from "react-router-dom";
const useStyles = makeStyles((theme: Theme) => {
return ({
contentPadding: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
}
})
});
interface Props {
group: Group;
isDialogOpen: boolean;
closeDialogCallback: Function;
}
export default function RemoveGroup(props: Props) {
const classes = useStyles();
const navigate = useNavigate();
const groupAPI = useGroupAPI();
const { success, error, warning } = useSnackbar();
const { group, isDialogOpen, closeDialogCallback } = props;
let groupName = group.name();
const closeDialog = () => {
closeDialogCallback();
};
const submit = () => {
groupAPI
.removeGroup(group.id())
.then((response: any) => {
success(groupName + " was successfully removed");
// navigate("/groups");
})
.catch((err: any) => {
err ? warning(err) : error("Error occured while removing " + groupName);
closeDialog();
});
};
return (
<Dialog
open={isDialogOpen}
fullWidth
onClose={closeDialog}
aria-labelledby="remove-group-label"
aria-describedby="remove-group-description">
<DialogTitle id="remove-group-title">Delete {groupName}?</DialogTitle>
<DialogContent className={classes.contentPadding}>
<Typography color="error" align="left" variant="subtitle1">
WARNING:
</Typography>
<Typography color="textPrimary" align="left" variant="body1">
Clicking 'Accept' will delete {groupName} for you and all other users.
</Typography>
<Typography color="textSecondary" align="left" variant="body2">
Devices will NOT be deleted in this process.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
Cancel
</Button>
<Button onClick={submit} color="primary">
Accept
</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -12,6 +12,8 @@ export class Group {
my.settings = pond.GroupSettings.fromObject(cloneDeep(or(pb.settings, {})));
my.status = pond.GroupStatus.fromObject(cloneDeep(or(pb.status, {})));
}
my.id = my.id.bind(my);
my.name = my.name.bind(my);
return my;
}

View file

@ -1,5 +1,5 @@
import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon } from "@mui/icons-material";
import { Box, Chip, IconButton, Theme, Tooltip, Typography } from "@mui/material";
import { Box, Chip, IconButton, Tab, Tabs, Theme, Tooltip, Typography } from "@mui/material";
import { blue, green } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
@ -15,6 +15,7 @@ import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { getDeviceStateHelper } from "pbHelpers/DeviceState";
import { Group } from "models";
import GroupSettings from "group/GroupSettings";
import SmartBreadcrumb from "common/SmartBreadcrumb";
const useStyles = makeStyles((theme: Theme) => {
return ({
@ -49,11 +50,12 @@ export default function Devices() {
const [devices, setDevices] = useState<pond.Device[]>([])
const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false);
const [groupLimit, setGroupLimit] = useState(10);
const [groupPage, setGroupPage] = useState(0);
const [groups, setGroups] = useState<Group[]>([])
const [groupLimit, ] = useState(10);
const [groupPage, ] = useState(0);
const [orderGroup, ] = useState<"asc" | "desc">("asc");
const [orderGroupBy, ] = useState("name");
const [searchGroup, setGroupSearch] = useState("");
const [searchGroup, ] = useState("");
const [totalGroups, setTotalGroups] = useState(0);
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(undefined);
@ -62,6 +64,8 @@ export default function Devices() {
>(undefined);
const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState<boolean>(false);
const [tab, setTab] = useState("all");
const openProvisionDialog = () => {
setIsProvisionDialogOpen(true);
};
@ -79,7 +83,7 @@ export default function Devices() {
setSelectedGroup(group);
};
const closeGroupSettings = (event: any) => {
const closeGroupSettings = (_event: any) => {
setGroupSettingsIsOpen(false);
setGroupSettingsMode(undefined);
setSelectedGroup(undefined);
@ -93,10 +97,20 @@ export default function Devices() {
orderGroupBy,
searchGroup,
).then(resp => {
console.log(resp.data)
// console.log(resp.data)
let newGroups: Group[] = []
resp.data.groups.forEach((group: pond.Group) => {
newGroups.push(Group.create(group))
})
setGroups(newGroups)
setTotalGroups(resp.data.total)
})
}
useEffect(() => {
console.log(groups)
}, [groups])
const loadDevices = () => {
deviceAPI.list(
limit,
@ -140,6 +154,11 @@ export default function Devices() {
navigate(appendToUrl(or(device.settings?.deviceId, "")), { state: {device: device} })
};
const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => {
if (newValue !== "never") setTab(newValue);
if (newValue === "never") openGroupSettings(undefined, "add");
};
const columns = (): Column<pond.Device>[] => {
return [
{
@ -170,7 +189,6 @@ export default function Devices() {
label={device.settings?.name ? device.settings.name : "Device " + device.settings?.deviceId}
/>
</Box>
)
}
},
@ -216,7 +234,7 @@ export default function Devices() {
)
}
const subtitle = () => {
const subtitle = () => {
return (
<>
{provisionButton()}
@ -227,6 +245,19 @@ export default function Devices() {
return(
<PageContainer padding={isMobile ? 0 : 2}>
<SmartBreadcrumb />
<Tabs
value={tab}
onChange={handleTabChange}
>
<Tab value={"all"} label="All" />
{groups.map((group, index) => {
if (group.id()) return (
<Tab value={group.id()} label={group.name()} key={"group-tab-"+index}/>
)
})}
<Tab value={"never"} label={groupButton()} />
</Tabs>
<ResponsiveTable<pond.Device>
title="Devices"
subtitle={subtitle()}