added group tabs:

This commit is contained in:
Carter 2025-01-02 11:08:15 -06:00
parent 26f0b7081b
commit bc8b0a3405
7 changed files with 380 additions and 41 deletions

View file

@ -70,6 +70,7 @@ export default function SmartBreadcrumb(props: Props) {
const groupID = (): string => { const groupID = (): string => {
const regexMatch = or(location.pathname.match(/\/groups\/([^/]+)/), []) const regexMatch = or(location.pathname.match(/\/groups\/([^/]+)/), [])
// console.log(or(regexMatch[1], "0").toString())
return 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 breadcrumbLinks = (): ReactNode[] => {
const breadcrumbMap = getBreadcrumbMap(); const breadcrumbMap = getBreadcrumbMap();
// console.log(breadcrumbMap)
let links: ReactNode[] = []; let links: ReactNode[] = [];
const pathnames = location.pathname.split("/").filter((x: any) => x); 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.length > 1) {
if (result[result.length - 1] !== "components") { if (result[result.length - 1] !== "components") {
label = breadcrumbMap["/" + result[result.length - 1]]; label = breadcrumbMap["/" + result[result.length - 1]];
console.log(label)
} }
} }
if (!label && result[result.length - 1] !== "components") { if (!label && result[result.length - 1] !== "components") {
label = breadcrumbMap["/" + result[result.length - 2] + "/" + result[result.length - 1]]; label = breadcrumbMap["/" + result[result.length - 2] + "/" + result[result.length - 1]];
console.log(label)
} }
} }

View file

@ -1,4 +1,4 @@
import { Suspense } from "react"; import { Suspense, useEffect, useState } from "react";
import LoadingScreen from "../app/LoadingScreen"; import LoadingScreen from "../app/LoadingScreen";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
@ -13,6 +13,9 @@ import Header from "app/Header";
import Logout from "pages/Logout"; import Logout from "pages/Logout";
import Devices from "pages/Devices"; import Devices from "pages/Devices";
import DevicePage from "pages/Device"; import DevicePage from "pages/Device";
import { Groups } from "@mui/icons-material";
import GroupsPage from "pages/Groups";
import { Group } from "models";
interface Props { interface Props {
open: boolean, open: boolean,
@ -21,11 +24,17 @@ interface Props {
toggleTheme: () => void; toggleTheme: () => void;
} }
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
return(`${basePath}/${appendage}`);
};
export default function Router(props: Props) { export default function Router(props: Props) {
const {open, onOpen, onClose, toggleTheme } = props; const {open, onOpen, onClose, toggleTheme } = props;
const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0(); const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0();
const isMobile = useMobile(); const isMobile = useMobile();
const [groups, setGroups] = useState<Group[]>([]);
const RelativeRoutes = () => { const RelativeRoutes = () => {
console.log("relative route") console.log("relative route")
@ -33,6 +42,7 @@ export default function Router(props: Props) {
<Routes> <Routes>
<Route path="teams/*" element={<TeamsRoute/>} /> <Route path="teams/*" element={<TeamsRoute/>} />
<Route path="devices/*" element={<DevicesRoute/>} /> <Route path="devices/*" element={<DevicesRoute/>} />
<Route path="groups/*" element={<GroupsRoute/>} />
</Routes> </Routes>
) )
} }
@ -59,17 +69,55 @@ export default function Router(props: Props) {
const DevicesRoute = () => { const DevicesRoute = () => {
console.log("devices route") console.log("devices route")
console.log(window.location.pathname)
return ( return (
<div> <div>
<Routes> <Routes>
<Route <Route
key="Devices page route"
path="" // "/settings/basic" path="" // "/settings/basic"
element={<Devices />} element={<Devices groups={groups} setGroups={setGroups} key={"Devices page"} />}
/> />
<Route <Route
path="/:deviceID" // "/settings/basic" path="/:deviceID" // "/settings/basic"
element={<DevicePage />} element={<DevicePage />}
/> />
<Route
path="/:deviceID/*" // "/settings/basic"
element={<RelativeRoutes />}
/>
</Routes>
</div>
);
};
useEffect(() => {
console.log(groups)
}, [groups])
const GroupsRoute = () => {
console.log("groups route")
console.log(window.location.pathname)
return (
<div>
<Routes>
<Route
path="/"
element={<GroupsPage />}
/>
<Route
key="Devices page route"
path="/:groupID/devices/"
element={<Devices groups={groups} setGroups={setGroups} key={"Devices page"}/>}
/>
<Route
path="/:groupID/devices/:deviceID"
element={<DevicePage />}
/>
<Route
path="/:groupID/devices/:deviceID/*"
element={<RelativeRoutes />}
/>
</Routes> </Routes>
</div> </div>
); );

View file

@ -21,6 +21,7 @@ const useStyles = makeStyles((theme: Theme) => {
}); });
export default function DevicePage() { export default function DevicePage() {
console.log("device")
const deviceAPI = useDeviceAPI() const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar() const snackbar = useSnackbar()
@ -51,7 +52,6 @@ export default function DevicePage() {
setPermissions(resp.data.permissions) setPermissions(resp.data.permissions)
let u = User.any(resp.data.user); let u = User.any(resp.data.user);
setPreferences(u.preferences) setPreferences(u.preferences)
console.log(u.preferences)
}).catch(err => { }).catch(err => {
setDevice(Device.create()); setDevice(Device.create());
// setComponents(new Map()); // setComponents(new Map());

View file

@ -1,5 +1,5 @@
import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon } from "@mui/icons-material"; 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 { blue, green } from "@mui/material/colors";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ResponsiveTable, { Column } from "common/ResponsiveTable";
@ -9,7 +9,7 @@ import { useDeviceAPI, useGroupAPI } from "providers";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import PageContainer from "./PageContainer"; import PageContainer from "./PageContainer";
import { useMobile } from "hooks"; 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 { or } from "utils/types";
import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { getDeviceStateHelper } from "pbHelpers/DeviceState"; import { getDeviceStateHelper } from "pbHelpers/DeviceState";
@ -19,6 +19,22 @@ import SmartBreadcrumb from "common/SmartBreadcrumb";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ 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: { provisionIcon: {
color: blue["700"] color: blue["700"]
}, },
@ -34,7 +50,13 @@ const useStyles = makeStyles((theme: Theme) => {
}); });
}); });
export default function Devices() { interface Props {
groups: Group[];
setGroups: React.Dispatch<React.SetStateAction<Group[]>>;
}
export default function Devices(props: Props) {
const { groups, setGroups } = props;
const isMobile = useMobile(); const isMobile = useMobile();
const classes = useStyles(); const classes = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
@ -52,13 +74,17 @@ export default function Devices() {
const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false); const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false);
const [groupsLoading, setGroupsLoading] = useState(false) const [groupsLoading, setGroupsLoading] = useState(false)
const [groups, setGroups] = useState<Group[]>([]) // const [groups, setGroups] = useState<Group[]>([]);
const [groupLimit, ] = useState(10); const [groupLimit, ] = useState(10);
const [groupPage, ] = useState(0); const [groupPage, ] = useState(0);
const [orderGroup, ] = useState<"asc" | "desc">("asc"); const [orderGroup, ] = useState<"asc" | "desc">("asc");
const [orderGroupBy, ] = useState("name"); const [orderGroupBy, ] = useState("name");
const [searchGroup, ] = useState(""); 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<Group | undefined>(undefined); const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(undefined);
const [groupSettingsMode, setGroupSettingsMode] = useState< const [groupSettingsMode, setGroupSettingsMode] = useState<
@ -66,7 +92,8 @@ export default function Devices() {
>(undefined); >(undefined);
const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState<boolean>(false); const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState<boolean>(false);
const [tab, setTab] = useState("all"); const groupID = useParams<{ groupID: string }>()?.groupID ?? "all";
const [tab, setTab] = useState(groupID==="all" ? groupID : groupID);
const openProvisionDialog = () => { const openProvisionDialog = () => {
setIsProvisionDialogOpen(true); setIsProvisionDialogOpen(true);
@ -90,8 +117,26 @@ export default function Devices() {
setGroupSettingsMode(undefined); setGroupSettingsMode(undefined);
setSelectedGroup(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 = () => { const loadGroups = () => {
if (groupsLoading) return
if (groups.length > 0) return
setGroupsLoading(true) setGroupsLoading(true)
groupAPI.listGroups( groupAPI.listGroups(
groupLimit, groupLimit,
@ -99,22 +144,19 @@ export default function Devices() {
orderGroup, orderGroup,
orderGroupBy, orderGroupBy,
searchGroup, searchGroup,
undefined
).then(resp => { ).then(resp => {
// console.log(resp.data)
let newGroups: Group[] = [] let newGroups: Group[] = []
resp.data.groups.forEach((group: pond.Group) => { resp.data.groups?.forEach((group: pond.Group) => {
newGroups.push(Group.create(group)) newGroups.push(Group.create(group))
}) })
setGroups(newGroups) updateGroups(newGroups)
setTotalGroups(resp.data.total) // setTotalGroups(resp.data.total)
}).finally(() => { }).finally(() => {
setGroupsLoading(false) setGroupsLoading(false)
}) })
} }
useEffect(() => {
console.log(groups)
}, [groups])
const loadDevices = () => { const loadDevices = () => {
setDevicesLoading(true) setDevicesLoading(true)
@ -130,8 +172,8 @@ export default function Devices() {
undefined, undefined,
undefined, undefined,
undefined, undefined,
getContextKeys(), getKeys(),
getContextTypes() getTypes()
).then(resp => { ).then(resp => {
setDevices(resp.data.devices) setDevices(resp.data.devices)
setTotal(resp.data.total) setTotal(resp.data.total)
@ -142,13 +184,13 @@ export default function Devices() {
useEffect(() => { useEffect(() => {
loadDevices() loadDevices()
}, [limit, page, order, orderBy, search]) }, [limit, page, order, orderBy, search, tab])
useEffect(() => { useEffect(() => {
// console.log("groups loaded")
loadGroups() loadGroups()
}, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup]) }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup])
const handleChange = (event: any) => { const handleChange = (event: any) => {
setLimit(event.target.value); setLimit(event.target.value);
}; };
@ -208,8 +250,8 @@ export default function Devices() {
<Box className={classes.cellContainer}> <Box className={classes.cellContainer}>
<Tooltip title={device.settings?.description}> <Tooltip title={device.settings?.description}>
<span> <span>
{description.length > 10 {description.length > 20
? description.substring(0, 10) + "..." ? description.substring(0, 20) + "..."
: description} : description}
</span> </span>
</Tooltip> </Tooltip>
@ -251,22 +293,104 @@ export default function Devices() {
) )
} }
// interface MyTabButtonProps {
// onClick?: React.MouseEventHandler<HTMLButtonElement>;
// disabled?: boolean;
// }
// const [leftButtonProps, setLeftButtonProps] = useState<MyTabButtonProps>({disabled: true})
// const [rightButtonProps, setRightButtonProps] = useState<MyTabButtonProps>({disabled: true})
// Use refs to store the props received during render
// const leftButtonRef = useRef<MyTabButtonProps>({});
// const rightButtonRef = useRef<MyTabButtonProps>({});
// 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( return(
<PageContainer padding={isMobile ? 0 : 2}> <PageContainer padding={isMobile ? 0 : 2}>
<SmartBreadcrumb /> <SmartBreadcrumb key={"Device page crumbs"}/>
<Tabs <Box
value={tab} // onMouseEnter={() => setTabsHovered(true)}
onChange={handleTabChange} // onMouseLeave={() => setTabsHovered(false)}
sx={{
borderRadius: 1,
border: "1px solid rgba(150, 150, 150, 0.2)",
marginTop: 1,
marginBottom: 1
}}
> >
<Tab value={"all"} label="All" /> <Tabs
{groups.map((group, index) => { {...{}}
if (group.id()) return ( value={groups.length > 0 ? tab : "all"}
<Tab value={group.id()} label={group.name()} key={"group-tab-"+index}/> onChange={handleTabChange}
) variant="scrollable"
})} scrollButtons="auto"
{groupsLoading && <CircularProgress />} sx={{ borderRadius: 1 }}
<Tab value={"never"} label={groupButton()} /> >
</Tabs> <Tab {...{}} value={"all"} label="All" />
{groupsLoading ?
<CircularProgress/>
: groups.map((group, index) => {
if (group.id()) return (
<Tab {...{}} value={group.id().toString()} label={group.name()} key={"group-tab-"+index}/>
)
})}
<Tab {...{}} value={"never"} label={<CreateGroupIcon className={classes.green} />} onClick={() => openGroupSettings(undefined, "add")} />
</Tabs>
</Box>
<ResponsiveTable<pond.Device> <ResponsiveTable<pond.Device>
title="Devices" title="Devices"
subtitle={subtitle()} subtitle={subtitle()}

25
src/pages/Group.tsx Normal file
View file

@ -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<Group>(state?.device ? Group.create(state.device) : Group.create())
return (
<Grid2 container justifyContent={"space-between"}>
<Grid2>
<SmartBreadcrumb groupName={group.name()} />
</Grid2>
<Grid2>
{/* <GroupActions /> */}
</Grid2>
</Grid2>
)
}

132
src/pages/Groups.tsx Normal file
View file

@ -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<pond.Group[]>([])
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<pond.Group>[] => {
return [
{
title: "Size",
render: (group) => {
return (
<Box className={classes.smallCellContainer}>
{group.settings?.devices?.length ? group.settings.devices.length : 0}
</Box>
)
}
},
{
title: "Group Name",
render: (group) => {
return (
<Box className={classes.cellContainer}>
<Chip
variant="outlined"
label={group.settings?.name ? group.settings.name : "Group " + group.settings?.groupId}
/>
</Box>
)
}
},
{
title: "Description",
render: device => {
const description = device.settings?.description ?? ""
return (
<Box className={classes.bigCellContainer}>
<Tooltip title={device.settings?.description}>
<Typography variant="body2">
{description}
</Typography>
</Tooltip>
</Box>
)
}
},
]
}
const handleRowClick = (row: pond.Group) => {
if (row.settings?.groupId) navigate(appendToUrl(or(row.settings?.groupId, "")), { state: {group: row} })
}
return (
<ResponsiveTable<pond.Group>
title={<SmartBreadcrumb />}
rows={groups}
columns={columns()}
total={total}
pageSize={limit}
page={page}
setPage={setPage}
handleRowsPerPageChange={handleRowsPerPageChange}
isLoading={loading}
setSearchText={setSearch}
onRowClick={handleRowClick}
/>
)
}

View file

@ -21,7 +21,9 @@ export interface IGroupAPIContext {
order?: "asc" | "desc", order?: "asc" | "desc",
orderBy?: string, orderBy?: string,
search?: string, search?: string,
asRoot?: boolean asRoot?: boolean,
keys?: string[],
types?: string[],
) => Promise<any>; ) => Promise<any>;
listGroupDevices: ( listGroupDevices: (
id: number, id: number,
@ -82,10 +84,18 @@ export default function GroupProvider(props: PropsWithChildren<Props>) {
order?: "asc" | "desc", order?: "asc" | "desc",
orderBy?: string, orderBy?: string,
search?: string, search?: string,
asRoot?: boolean asRoot?: boolean,
keys?: string[],
types?: string[]
) => { ) => {
let keys = getContextKeys() keys = keys ? keys : getContextKeys();
let types = getContextTypes() types = types? types : getContextTypes();
types.forEach((type, index) => {
if (type === "group") {
types.splice(index, 1)
keys.splice(index, 1)
}
})
return get( return get(
pondURL( pondURL(
"/groups" + "/groups" +