From 916d0f913234826ee4f675992bcd53530be16afb Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 19 Dec 2024 14:10:16 -0600 Subject: [PATCH] groupd now generate tabs --- src/group/GroupSettings.tsx | 484 ++++++++++++++++++++++++++++++++++++ src/group/RemoveGroup.tsx | 84 +++++++ src/models/Group.ts | 2 + src/pages/Devices.tsx | 47 +++- 4 files changed, 609 insertions(+), 8 deletions(-) create mode 100644 src/group/GroupSettings.tsx create mode 100644 src/group/RemoveGroup.tsx diff --git a/src/group/GroupSettings.tsx b/src/group/GroupSettings.tsx new file mode 100644 index 0000000..21b3e57 --- /dev/null +++ b/src/group/GroupSettings.tsx @@ -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([]); + const [group, setGroup] = useState(initialGroup ? Group.clone(initialGroup) : new Group()); + const [isRemoveGroupOpen, setIsRemoveGroupOpen] = useState( + mode === "remove" ? true : false + ); + const [tabIndex, setTabIndex] = useState(0); + const prevTabIndex = usePrevious(tabIndex); + const [steps] = useState(["General group information", "Select devices for the group"]); + const [deviceSearch, setDeviceSearch] = useState(""); + const [loadingDevices, setLoadingDevices] = useState(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 ( + + Group Creation + + Groups are a great way to organize your devices + + + ); + case "update": + return ( + + Group Settings + + {group.name()} + + + ); + default: + return ; + } + }; + + const generalGroupContent = () => { + return ( + + + + + + ); + }; + + const devicesGroupContent = () => { + if (mode !== "add") return devicesTab(); + return ( + + + {loadingDevices ? ( + + + + + + + + ) : ( + + {filterDevices(deviceSearch, devices, []) + .sort((a, b: Device) => a.name().localeCompare(b.name())) + .map(device => { + const label = `checkbox-list-secondary-label-${device.id()}`; + return ( + changeDevices(device.id())}> + + + changeDevices(device.id())} + checked={group.settings.devices.includes(device.id())} + inputProps={{ "aria-labelledby": label }} + disabled={!canEdit} + /> + + + ); + })} + + )} + + ); + }; + + //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 ( + + + {loadingDevices ? ( + + + + + + + + ) : ( + + {filterDevices(deviceSearch, devices, []) + .sort((a, b: Device) => a.name().localeCompare(b.name())) + .map(device => { + const label = `checkbox-list-secondary-label-${device.id()}`; + return ( + changeDevices(device.id())}> + + + { + if (checked) addDevice(device.id()); + else removeDevice(device.id()); + }} + checked={Boolean(groupDevices?.find(dev => dev.id() === device.id()))} + inputProps={{ "aria-labelledby": label }} + disabled={!canEdit} + /> + + + ); + })} + + )} + + ); + }; + + const content = () => { + if (mode === "add") { + return ( + + {tabIndex === 0 && generalGroupContent()} + {tabIndex === 1 && devicesGroupContent()} + + {steps.map(label => ( + + {label} + + ))} + + + + ); + } else if (mode === "update") { + return ( + + {tabIndex === 0 && generalGroupContent()} + {tabIndex === 1 && devicesGroupContent()} + + ); + } + + return ; + }; + + const actions = () => { + return ( + + + {mode === "add" ? ( + + ) : ( + mode === "update" && + canEdit && Delete + )} + + + + {(mode === "add" && tabIndex === steps.length - 1) || mode === "update" + ? canEdit && ( + + ) + : mode === "add" && ( + + )} + + + ); + }; + + const dialogs = () => { + return ( + + ); + }; + + return ( + + {title()} + {mode === "update" && ( + { + changeTab(value); + }} + indicatorColor="secondary" + textColor="inherit" + variant="fullWidth"> + + + + )} + + {content()} + {actions()} + {dialogs()} + + ); +} diff --git a/src/group/RemoveGroup.tsx b/src/group/RemoveGroup.tsx new file mode 100644 index 0000000..de05b38 --- /dev/null +++ b/src/group/RemoveGroup.tsx @@ -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 ( + + Delete {groupName}? + + + WARNING: + + + Clicking 'Accept' will delete {groupName} for you and all other users. + + + Devices will NOT be deleted in this process. + + + + + + + + ); +} diff --git a/src/models/Group.ts b/src/models/Group.ts index 4e9bea6..02c4ded 100644 --- a/src/models/Group.ts +++ b/src/models/Group.ts @@ -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; } diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 7b40f03..7602832 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -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([]) const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); - const [groupLimit, setGroupLimit] = useState(10); - const [groupPage, setGroupPage] = useState(0); + const [groups, setGroups] = useState([]) + 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(undefined); @@ -62,6 +64,8 @@ export default function Devices() { >(undefined); const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState(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[] => { return [ { @@ -170,7 +189,6 @@ export default function Devices() { label={device.settings?.name ? device.settings.name : "Device " + device.settings?.deviceId} /> - ) } }, @@ -216,7 +234,7 @@ export default function Devices() { ) } - const subtitle = () => { + const subtitle = () => { return ( <> {provisionButton()} @@ -227,6 +245,19 @@ export default function Devices() { return( + + + + {groups.map((group, index) => { + if (group.id()) return ( + + ) + })} + + title="Devices" subtitle={subtitle()}