From bc8b0a34053c730dc62dc27e79f5057f9dde4df4 Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 2 Jan 2025 11:08:15 -0600 Subject: [PATCH] added group tabs: --- src/common/SmartBreadcrumb.tsx | 4 +- src/navigation/Router.tsx | 52 ++++++++- src/pages/Device.tsx | 2 +- src/pages/Devices.tsx | 188 ++++++++++++++++++++++++++------ src/pages/Group.tsx | 25 +++++ src/pages/Groups.tsx | 132 ++++++++++++++++++++++ src/providers/pond/groupAPI.tsx | 18 ++- 7 files changed, 380 insertions(+), 41 deletions(-) create mode 100644 src/pages/Group.tsx create mode 100644 src/pages/Groups.tsx diff --git a/src/common/SmartBreadcrumb.tsx b/src/common/SmartBreadcrumb.tsx index 7661f5f..1f9a6ae 100644 --- a/src/common/SmartBreadcrumb.tsx +++ b/src/common/SmartBreadcrumb.tsx @@ -70,6 +70,7 @@ export default function SmartBreadcrumb(props: Props) { const groupID = (): string => { const regexMatch = or(location.pathname.match(/\/groups\/([^/]+)/), []) + // console.log(or(regexMatch[1], "0").toString()) return or(regexMatch[1], "0").toString(); }; @@ -244,6 +245,7 @@ export default function SmartBreadcrumb(props: Props) { const breadcrumbLinks = (): ReactNode[] => { const breadcrumbMap = getBreadcrumbMap(); + // console.log(breadcrumbMap) let links: ReactNode[] = []; const pathnames = location.pathname.split("/").filter((x: any) => x); @@ -266,12 +268,10 @@ export default function SmartBreadcrumb(props: Props) { if (result.length > 1) { if (result[result.length - 1] !== "components") { label = breadcrumbMap["/" + result[result.length - 1]]; - console.log(label) } } if (!label && result[result.length - 1] !== "components") { label = breadcrumbMap["/" + result[result.length - 2] + "/" + result[result.length - 1]]; - console.log(label) } } diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 488d7e4..a33cf2e 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react"; +import { Suspense, useEffect, useState } from "react"; import LoadingScreen from "../app/LoadingScreen"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { Typography } from "@mui/material"; @@ -13,6 +13,9 @@ import Header from "app/Header"; import Logout from "pages/Logout"; import Devices from "pages/Devices"; import DevicePage from "pages/Device"; +import { Groups } from "@mui/icons-material"; +import GroupsPage from "pages/Groups"; +import { Group } from "models"; interface Props { open: boolean, @@ -21,11 +24,17 @@ interface Props { toggleTheme: () => void; } +export const appendToUrl = (appendage: number | string) => { + const basePath = location.pathname.replace(/\/$/, ""); + return(`${basePath}/${appendage}`); +}; + export default function Router(props: Props) { const {open, onOpen, onClose, toggleTheme } = props; const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0(); const isMobile = useMobile(); + const [groups, setGroups] = useState([]); const RelativeRoutes = () => { console.log("relative route") @@ -33,6 +42,7 @@ export default function Router(props: Props) { } /> } /> + } /> ) } @@ -59,17 +69,55 @@ export default function Router(props: Props) { const DevicesRoute = () => { console.log("devices route") + console.log(window.location.pathname) return (
} + element={} /> } /> + } + /> + +
+ ); + }; + + useEffect(() => { + console.log(groups) + }, [groups]) + + const GroupsRoute = () => { + console.log("groups route") + console.log(window.location.pathname) + return ( +
+ + } + /> + } + /> + } + /> + } + />
); diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 443ce00..ffcd532 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -21,6 +21,7 @@ const useStyles = makeStyles((theme: Theme) => { }); export default function DevicePage() { + console.log("device") const deviceAPI = useDeviceAPI() const snackbar = useSnackbar() @@ -51,7 +52,6 @@ export default function DevicePage() { setPermissions(resp.data.permissions) let u = User.any(resp.data.user); setPreferences(u.preferences) - console.log(u.preferences) }).catch(err => { setDevice(Device.create()); // setComponents(new Map()); diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index ed7210d..e5ba4d5 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, CircularProgress, IconButton, Tab, Tabs, Theme, Tooltip, Typography } from "@mui/material"; +import { Box, Chip, CircularProgress, IconButton, Tab, Tabs, Theme, Tooltip } from "@mui/material"; import { blue, green } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; @@ -9,7 +9,7 @@ import { useDeviceAPI, useGroupAPI } from "providers"; import { useEffect, useState } from "react"; import PageContainer from "./PageContainer"; import { useMobile } from "hooks"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; import { or } from "utils/types"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { getDeviceStateHelper } from "pbHelpers/DeviceState"; @@ -19,6 +19,22 @@ import SmartBreadcrumb from "common/SmartBreadcrumb"; const useStyles = makeStyles((theme: Theme) => { return ({ + buttonBox: { + position: "absolute", + zIndex: 1, + right: theme.spacing(4), + transform: "translateY(-50%)", // Move it up by half its height + transition: "opacity 0.2s ease-in-out", + opacity: 1, + // top: 0, + }, + fadeBox: { + opacity: 0, // Fully transparent by default + transition: "opacity 0.2s ease-in-out", // Smooth transition + "&:hover": { + opacity: 1, // Fully opaque on hover + }, + }, provisionIcon: { color: blue["700"] }, @@ -34,7 +50,13 @@ const useStyles = makeStyles((theme: Theme) => { }); }); -export default function Devices() { +interface Props { + groups: Group[]; + setGroups: React.Dispatch>; +} + +export default function Devices(props: Props) { + const { groups, setGroups } = props; const isMobile = useMobile(); const classes = useStyles(); const navigate = useNavigate(); @@ -52,13 +74,17 @@ export default function Devices() { const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); const [groupsLoading, setGroupsLoading] = useState(false) - const [groups, setGroups] = useState([]) + // const [groups, setGroups] = useState([]); const [groupLimit, ] = useState(10); const [groupPage, ] = useState(0); const [orderGroup, ] = useState<"asc" | "desc">("asc"); const [orderGroupBy, ] = useState("name"); const [searchGroup, ] = useState(""); - const [totalGroups, setTotalGroups] = useState(0); + // const [totalGroups, setTotalGroups] = useState(0); + + const updateGroups = (newGroups: Group[]) => { + setGroups(newGroups); // Trigger UI update if needed + }; const [selectedGroup, setSelectedGroup] = useState(undefined); const [groupSettingsMode, setGroupSettingsMode] = useState< @@ -66,7 +92,8 @@ export default function Devices() { >(undefined); const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState(false); - const [tab, setTab] = useState("all"); + const groupID = useParams<{ groupID: string }>()?.groupID ?? "all"; + const [tab, setTab] = useState(groupID==="all" ? groupID : groupID); const openProvisionDialog = () => { setIsProvisionDialogOpen(true); @@ -90,8 +117,26 @@ export default function Devices() { setGroupSettingsMode(undefined); setSelectedGroup(undefined); }; + + const getKeys = () => { + if (tab !== "all") { + let keys = getContextKeys() + keys.splice(keys.length - 1, 0, tab); + return keys + } + } + + const getTypes = () => { + if (tab !== "all") { + let types = getContextTypes() + types.splice(types.length - 1, 0, "group"); + return types + } + } const loadGroups = () => { + if (groupsLoading) return + if (groups.length > 0) return setGroupsLoading(true) groupAPI.listGroups( groupLimit, @@ -99,22 +144,19 @@ export default function Devices() { orderGroup, orderGroupBy, searchGroup, + undefined ).then(resp => { - // console.log(resp.data) let newGroups: Group[] = [] - resp.data.groups.forEach((group: pond.Group) => { + resp.data.groups?.forEach((group: pond.Group) => { newGroups.push(Group.create(group)) }) - setGroups(newGroups) - setTotalGroups(resp.data.total) + updateGroups(newGroups) + // setTotalGroups(resp.data.total) }).finally(() => { setGroupsLoading(false) }) } - useEffect(() => { - console.log(groups) - }, [groups]) const loadDevices = () => { setDevicesLoading(true) @@ -130,8 +172,8 @@ export default function Devices() { undefined, undefined, undefined, - getContextKeys(), - getContextTypes() + getKeys(), + getTypes() ).then(resp => { setDevices(resp.data.devices) setTotal(resp.data.total) @@ -142,13 +184,13 @@ export default function Devices() { useEffect(() => { loadDevices() - }, [limit, page, order, orderBy, search]) + }, [limit, page, order, orderBy, search, tab]) useEffect(() => { + // console.log("groups loaded") loadGroups() }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup]) - const handleChange = (event: any) => { setLimit(event.target.value); }; @@ -208,8 +250,8 @@ export default function Devices() { - {description.length > 10 - ? description.substring(0, 10) + "..." + {description.length > 20 + ? description.substring(0, 20) + "..." : description} @@ -251,22 +293,104 @@ export default function Devices() { ) } + // interface MyTabButtonProps { + // onClick?: React.MouseEventHandler; + // disabled?: boolean; + // } + // const [leftButtonProps, setLeftButtonProps] = useState({disabled: true}) + // const [rightButtonProps, setRightButtonProps] = useState({disabled: true}) + // Use refs to store the props received during render + // const leftButtonRef = useRef({}); + // const rightButtonRef = useRef({}); + // const [tabsHovered, setTabsHovered] = useState(false) + // const location = useLocation() + + // Update state using useEffect based on ref changes (the props received) + // useEffect(() => { + // if (leftButtonRef.current.disabled !== leftButtonProps.disabled) { + // setLeftButtonProps({ disabled: leftButtonRef.current.disabled, onClick: leftButtonRef.current.onClick }); + // } + // if (rightButtonRef.current.disabled !== rightButtonProps.disabled) { + // setRightButtonProps({ disabled: rightButtonRef.current.disabled, onClick: rightButtonRef.current.onClick }); + // } + // }, [leftButtonRef.current, rightButtonRef.current]); + + // const handleScrollButtonProps = (props: TabScrollButtonProps) => { + // if ((props.direction === "left" && + // leftButtonProps.disabled !== props.disabled) + // ) { + // let newProps: any = {} + // newProps.disabled = props.disabled + // newProps.onClick = props.onClick + // setLeftButtonProps(newProps) + // // leftButtonRef.current = newProps + // } + // if (props.direction === "right" && + // rightButtonProps.disabled !== props.disabled + // ) { + // let newProps: any = {} + // newProps.disabled = props.disabled + // newProps.onClick = props.onClick + // setRightButtonProps(newProps) + // // rightButtonRef.current = newProps + // } + // } + + // useEffect(() => { + // if (tab !== "all") { + // const group = groups[typeof(tab) === "number" ? tab-1 : parseInt(tab)-1] + // const url = location.pathname; + // if (group) { + // const updatedUrl = url.replace( + // /(\/groups\/[^/]+)?\/devices$/, // Matches "/groups/some-id/devices" or just "/devices" + // `/groups/${group.id()}/devices` + // ); + // navigate(updatedUrl, { replace: true }); + // loadDevices() + // } + // } else { + // const url = location.pathname; + // const updatedUrl = url.replace( + // /(\/groups\/[^/]+)?\/devices$/, // Matches "/groups/some-id/devices" or just "/devices" + // `/devices` + // ); + // navigate(updatedUrl, { replace: true }); + // loadDevices() + // } + // }, [tab]) + return( - - + setTabsHovered(true)} + // onMouseLeave={() => setTabsHovered(false)} + sx={{ + borderRadius: 1, + border: "1px solid rgba(150, 150, 150, 0.2)", + marginTop: 1, + marginBottom: 1 + }} > - - {groups.map((group, index) => { - if (group.id()) return ( - - ) - })} - {groupsLoading && } - - + 0 ? tab : "all"} + onChange={handleTabChange} + variant="scrollable" + scrollButtons="auto" + sx={{ borderRadius: 1 }} + > + + {groupsLoading ? + + : groups.map((group, index) => { + if (group.id()) return ( + + ) + })} + } onClick={() => openGroupSettings(undefined, "add")} /> + + title="Devices" subtitle={subtitle()} diff --git a/src/pages/Group.tsx b/src/pages/Group.tsx new file mode 100644 index 0000000..edefc31 --- /dev/null +++ b/src/pages/Group.tsx @@ -0,0 +1,25 @@ +import { Grid2 } from "@mui/material"; +import SmartBreadcrumb from "common/SmartBreadcrumb"; +import { Group } from "models" +import { useState } from "react" +import { useLocation, useParams } from "react-router-dom"; + + +export default function GroupPage() { + + const groupID = useParams<{ groupID: string }>()?.groupID ?? ""; + const { state } = useLocation(); + const [group, setGroup] = useState(state?.device ? Group.create(state.device) : Group.create()) + + + return ( + + + + + + {/* */} + + + ) +} \ No newline at end of file diff --git a/src/pages/Groups.tsx b/src/pages/Groups.tsx new file mode 100644 index 0000000..61dfd5e --- /dev/null +++ b/src/pages/Groups.tsx @@ -0,0 +1,132 @@ +import { Box, Chip, Theme, Tooltip, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import ResponsiveTable, { Column } from "common/ResponsiveTable"; +import SmartBreadcrumb from "common/SmartBreadcrumb"; +import { useMobile } from "hooks"; +import { appendToUrl } from "navigation/Router"; +import { pond } from "protobuf-ts/pond"; +import { useGroupAPI } from "providers"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { or } from "utils/types"; + +const useStyles = makeStyles((theme: Theme) => { + const isMobile = useMobile() + return ({ + cellContainer: { + margin: theme.spacing(1), + marginLeft: theme.spacing(2), + display: "flex", + maxWidth: theme.spacing(isMobile ? 18 : 32), + }, + bigCellContainer: { + margin: theme.spacing(1), + marginLeft: theme.spacing(2), + display: "flex", + width: "auto", + }, + smallCellContainer: { + margin: theme.spacing(1), + marginLeft: theme.spacing(2), + display: "flex", + width: theme.spacing(1), + }, + }); +}); + +export default function GroupsPage() { + const groupAPI = useGroupAPI() + const navigate = useNavigate() + const [limit, setLimit] = useState(10) + const [page, setPage] = useState(0) + const [order, setOrder] = useState<"asc" | "desc">("asc") + const [orderBy, setOrderBy] = useState("") + const [search, setSearch] = useState("") + const [total, setTotal] = useState(0) + const [loading, setLoading] = useState(false) + + const [groups, setGroups] = useState([]) + + const classes = useStyles() + + const loadGroups = () => { + setLoading(true) + groupAPI.listGroups(limit, page*limit, order, orderBy, search).then(resp => { + setGroups(resp.data.groups) + setTotal(resp.data.total) + }).finally(() => { + setLoading(false) + }) + } + + useEffect(() => { + loadGroups() + }, [limit, page, order, orderBy, search]) + + const handleRowsPerPageChange = (event: any) => { + setLimit(event.target.value); + }; + + const columns = (): Column[] => { + return [ + { + title: "Size", + render: (group) => { + return ( + + {group.settings?.devices?.length ? group.settings.devices.length : 0} + + ) + } + }, + { + title: "Group Name", + render: (group) => { + return ( + + + + ) + } + }, + { + title: "Description", + render: device => { + const description = device.settings?.description ?? "" + return ( + + + + {description} + + + + ) + } + }, + ] + } + + const handleRowClick = (row: pond.Group) => { + if (row.settings?.groupId) navigate(appendToUrl(or(row.settings?.groupId, "")), { state: {group: row} }) + } + + return ( + + title={} + rows={groups} + columns={columns()} + total={total} + pageSize={limit} + page={page} + setPage={setPage} + handleRowsPerPageChange={handleRowsPerPageChange} + isLoading={loading} + setSearchText={setSearch} + onRowClick={handleRowClick} + /> + ) +} \ No newline at end of file diff --git a/src/providers/pond/groupAPI.tsx b/src/providers/pond/groupAPI.tsx index 6830071..407cb7e 100644 --- a/src/providers/pond/groupAPI.tsx +++ b/src/providers/pond/groupAPI.tsx @@ -21,7 +21,9 @@ export interface IGroupAPIContext { order?: "asc" | "desc", orderBy?: string, search?: string, - asRoot?: boolean + asRoot?: boolean, + keys?: string[], + types?: string[], ) => Promise; listGroupDevices: ( id: number, @@ -82,10 +84,18 @@ export default function GroupProvider(props: PropsWithChildren) { order?: "asc" | "desc", orderBy?: string, search?: string, - asRoot?: boolean + asRoot?: boolean, + keys?: string[], + types?: string[] ) => { - let keys = getContextKeys() - let types = getContextTypes() + keys = keys ? keys : getContextKeys(); + types = types? types : getContextTypes(); + types.forEach((type, index) => { + if (type === "group") { + types.splice(index, 1) + keys.splice(index, 1) + } + }) return get( pondURL( "/groups" +