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 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)
}
}

View file

@ -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<Group[]>([]);
const RelativeRoutes = () => {
console.log("relative route")
@ -33,6 +42,7 @@ export default function Router(props: Props) {
<Routes>
<Route path="teams/*" element={<TeamsRoute/>} />
<Route path="devices/*" element={<DevicesRoute/>} />
<Route path="groups/*" element={<GroupsRoute/>} />
</Routes>
)
}
@ -59,17 +69,55 @@ export default function Router(props: Props) {
const DevicesRoute = () => {
console.log("devices route")
console.log(window.location.pathname)
return (
<div>
<Routes>
<Route
key="Devices page route"
path="" // "/settings/basic"
element={<Devices />}
element={<Devices groups={groups} setGroups={setGroups} key={"Devices page"} />}
/>
<Route
path="/:deviceID" // "/settings/basic"
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>
</div>
);

View file

@ -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());

View file

@ -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<React.SetStateAction<Group[]>>;
}
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<boolean>(false);
const [groupsLoading, setGroupsLoading] = useState(false)
const [groups, setGroups] = useState<Group[]>([])
// 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, ] = 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 [groupSettingsMode, setGroupSettingsMode] = useState<
@ -66,7 +92,8 @@ export default function Devices() {
>(undefined);
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 = () => {
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() {
<Box className={classes.cellContainer}>
<Tooltip title={device.settings?.description}>
<span>
{description.length > 10
? description.substring(0, 10) + "..."
{description.length > 20
? description.substring(0, 20) + "..."
: description}
</span>
</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(
<PageContainer padding={isMobile ? 0 : 2}>
<SmartBreadcrumb />
<Tabs
value={tab}
onChange={handleTabChange}
<SmartBreadcrumb key={"Device page crumbs"}/>
<Box
// onMouseEnter={() => setTabsHovered(true)}
// onMouseLeave={() => setTabsHovered(false)}
sx={{
borderRadius: 1,
border: "1px solid rgba(150, 150, 150, 0.2)",
marginTop: 1,
marginBottom: 1
}}
>
<Tab value={"all"} label="All" />
{groups.map((group, index) => {
if (group.id()) return (
<Tab value={group.id()} label={group.name()} key={"group-tab-"+index}/>
)
})}
{groupsLoading && <CircularProgress />}
<Tab value={"never"} label={groupButton()} />
</Tabs>
<Tabs
{...{}}
value={groups.length > 0 ? tab : "all"}
onChange={handleTabChange}
variant="scrollable"
scrollButtons="auto"
sx={{ borderRadius: 1 }}
>
<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>
title="Devices"
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",
orderBy?: string,
search?: string,
asRoot?: boolean
asRoot?: boolean,
keys?: string[],
types?: string[],
) => Promise<any>;
listGroupDevices: (
id: number,
@ -82,10 +84,18 @@ export default function GroupProvider(props: PropsWithChildren<Props>) {
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" +