committing all group related code

This commit is contained in:
Carter 2025-01-07 10:34:18 -06:00
parent 0ec54b2d31
commit 2d98917322
14 changed files with 541 additions and 102 deletions

View file

@ -20,12 +20,21 @@ const useStyles = makeStyles((theme: Theme) => {
// width: "auto"
},
title: {
padding: theme.spacing(1)
padding: theme.spacing(1),
},
subtitle: {
padding: theme.spacing(1),
marginTop: theme.spacing(-2),
},
titleComp: {
padding: theme.spacing(1),
marginLeft: theme.spacing(-1)
},
subtitleComp: {
padding: theme.spacing(1),
marginTop: theme.spacing(-2),
marginLeft: theme.spacing(-1),
},
titleContainer: {
border: "none",
},
@ -184,7 +193,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
</Typography>
)
if (title) return (
<Box className={classes.title}>
<Box className={classes.titleComp}>
{title}
</Box>
)
@ -198,7 +207,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
</Typography>
)
if (subtitle) return (
<Box className={classes.subtitle}>
<Box className={classes.subtitleComp}>
{subtitle}
</Box>
)

View file

@ -1,4 +1,5 @@
import {
Box,
Breadcrumbs,
Chip,
IconButton,
@ -13,7 +14,7 @@ import { Replay } from "@mui/icons-material";
// import { Skeleton } from "@mui/l";
import { useMobile } from "hooks";
// import { MatchParams } from "navigation/Routes";
import React, { ReactNode } from "react";
import { ReactNode } from "react";
import { useLocation } from "react-router";
import { Link as RouterLink } from "react-router-dom";
import { or } from "utils/types";
@ -52,6 +53,11 @@ interface Props {
reportTarget?: string;
loading?: boolean;
reload?: () => void;
padding?: number;
paddingLeft?: number;
paddingRight?: number;
paddingTop?: number;
paddingBottom?: number;
}
interface LinkRouterProps extends LinkProps {
@ -251,19 +257,17 @@ export default function SmartBreadcrumb(props: Props) {
const pathnames = location.pathname.split("/").filter((x: any) => x);
const blacklist = breadcrumbBlacklist();
// console.log(pathnames)
if (pathnames.length < 1) {
return [linkComponent("/", "Dashboard", true)];
}
pathnames.forEach((_value: any, index: any) => {
const lastPath = index === pathnames.length - 1;
const to = `/${pathnames.slice(0, index + 1).join("/")}`;
let label = breadcrumbMap[to];
let result = to.split("/").filter(str => str !== "");
// console.log(result)
if (!label) {
if (result.length > 1) {
if (result[result.length - 1] !== "components") {
@ -278,9 +282,8 @@ export default function SmartBreadcrumb(props: Props) {
if (!blacklist.includes(to) && label) {
links.push(linkComponent(to, label, lastPath));
}
});
// console.log(links)
});
return links;
};
@ -315,7 +318,13 @@ export default function SmartBreadcrumb(props: Props) {
if (loading === true) return <Skeleton variant="text" width="100%" />;
return (
<React.Fragment>
<Box
padding={props.padding}
paddingLeft={props.paddingLeft}
paddingRight={props.paddingRight}
paddingTop={props.paddingTop}
paddingBottom={props.paddingBottom}
>
<Breadcrumbs
maxItems={isMobile ? 1 : 3}
itemsBeforeCollapse={0}
@ -331,6 +340,6 @@ export default function SmartBreadcrumb(props: Props) {
<Replay />
</IconButton>
)}
</React.Fragment>
</Box>
);
}

204
src/group/GroupActions.tsx Normal file
View file

@ -0,0 +1,204 @@
import {
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Theme,
Tooltip
} from "@mui/material";
import { blue } from "@mui/material/colors";
import RemoveSelfIcon from "@mui/icons-material/ExitToApp";
import MoreIcon from "@mui/icons-material/MoreVert";
import GroupSettingsIcon from "@mui/icons-material/Settings";
import ShareObjectIcon from "@mui/icons-material/Share";
import ObjectUsersIcon from "@mui/icons-material/AccountCircle";
import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle";
import GroupSettings from "group/GroupSettings";
import { Device, Group, groupScope } from "models";
import { pond } from "protobuf-ts/pond";
import { useState } from "react";
// import ObjectTeams from "teams/ObjectTeams";
import ObjectUsers from "user/ObjectUsers";
import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import { makeStyles } from "@mui/styles";
// const useStyles = makeStyles((theme: Theme) => {
// // const isMobile = useMobile()
// return ({
// });
// });
const useStyles = makeStyles((theme: Theme) => {
return ({
shareIcon: {
color: blue["500"],
"&:hover": {
color: blue["600"]
}
},
removeIcon: {
color: "var(--status-alert)"
},
red: {
color: "var(--status-alert)"
}
});
});
interface Props {
group: Group;
devices: Device[];
permissions: pond.Permission[];
refreshCallback: () => void;
}
export default function GroupActions(props: Props) {
const classes = useStyles();
const { group, permissions, refreshCallback, devices } = props;
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [groupSettingsOpen, setGroupSettingsOpen] = useState(false);
const [shareOpen, setShareOpen] = useState(false);
const [usersOpen, setUsersOpen] = useState(false);
const [teamsOpen, setTeamsOpen] = useState(false);
const [removeSelfOpen, setRemoveSelfOpen] = useState(false);
const groupMenu = () => {
const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE);
const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS);
const canTeams = canManageUsers;
return (
<Menu
id="groupMenu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
keepMounted
disableAutoFocusItem>
{!isOffline() && canShare && (
<MenuItem
dense
onClick={() => {
setShareOpen(true);
setAnchorEl(null);
}}
>
<ListItemIcon>
<ShareObjectIcon className={classes.shareIcon} />
</ListItemIcon>
<ListItemText primary="Share" />
</MenuItem>
)}
{!isOffline() && canManageUsers && (
<MenuItem
dense
onClick={() => {
setUsersOpen(true);
setAnchorEl(null);
}}
>
<ListItemIcon>
<ObjectUsersIcon />
</ListItemIcon>
<ListItemText primary="Users" />
</MenuItem>
)}
{!isOffline() && canTeams && (
<MenuItem
dense
onClick={() => {
setTeamsOpen(true);
setAnchorEl(null);
}}
>
<ListItemIcon>
<ObjectTeamsIcon />
</ListItemIcon>
<ListItemText primary="Teams" />
</MenuItem>
)}
<MenuItem
dense
onClick={() => {
setRemoveSelfOpen(true);
setAnchorEl(null);
}}
>
<ListItemIcon>
<RemoveSelfIcon className={classes.red} />
</ListItemIcon>
<ListItemText primary="Leave" />
</MenuItem>
</Menu>
);
};
const dialogs = () => {
const hasWritePermission = permissions.includes(pond.Permission.PERMISSION_WRITE);
const key = group.id().toString();
const label = group.name();
return (
<>
<GroupSettings
mode="update"
initialGroup={group}
canEdit={hasWritePermission}
isDialogOpen={groupSettingsOpen}
refreshCallback={refreshCallback}
closeDialogCallback={() => setGroupSettingsOpen(false)}
groupDevices={devices}
/>
<ShareObject
scope={groupScope(key)}
label={label}
permissions={permissions}
isDialogOpen={shareOpen}
closeDialogCallback={() => setShareOpen(false)}
/>
<ObjectUsers
scope={groupScope(key)}
label={label}
permissions={permissions}
isDialogOpen={usersOpen}
closeDialogCallback={() => setUsersOpen(false)}
refreshCallback={refreshCallback}
/>
{/* <ObjectTeams
scope={groupScope(key)}
label={label}
permissions={permissions}
isDialogOpen={teamsOpen}
closeDialogCallback={() => setTeamsOpen(false)}
refreshCallback={refreshCallback}
/> */}
<RemoveSelfFromObject
scope={groupScope(key)}
label={label}
isDialogOpen={removeSelfOpen}
closeDialogCallback={() => setRemoveSelfOpen(false)}
/>
</>
);
};
return (
<>
<Tooltip title="Group Settings">
<IconButton onClick={() => setGroupSettingsOpen(true)}>
<GroupSettingsIcon />
</IconButton>
</Tooltip>
<IconButton
aria-owns={anchorEl ? "groupMenu" : undefined}
aria-haspopup="true"
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
<MoreIcon />
</IconButton>
{groupMenu()}
{dialogs()}
</>
);
}

View file

@ -14,6 +14,7 @@ import Logout from "pages/Logout";
import Devices from "pages/Devices";
import DevicePage from "pages/Device";
import GroupsPage from "pages/Groups";
import GroupPage from "pages/Group";
interface Props {
open: boolean,
@ -90,7 +91,6 @@ export default function Router(props: Props) {
const GroupsRoute = () => {
console.log("groups route")
console.log(window.location.pathname)
return (
<div>
<Routes>
@ -98,6 +98,10 @@ export default function Router(props: Props) {
path="/"
element={<GroupsPage />}
/>
<Route
path="/:groupID"
element={<GroupPage />}
/>
<Route
key="Devices page route"
path="/:groupID/devices/"

View file

@ -21,8 +21,6 @@ const useStyles = makeStyles((theme: Theme) => {
});
export default function DevicePage() {
console.log("device")
const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar()
const isMobile = useMobile()

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 } from "@mui/material";
import { Box, Chip, CircularProgress, 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";
@ -47,6 +47,14 @@ const useStyles = makeStyles((theme: Theme) => {
green: {
color: green["600"]
},
tab: {
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: theme.spacing(26),
whiteSpace: "nowrap",
padding: theme.spacing(1.5)
}
});
});
@ -78,7 +86,7 @@ export default function Devices() {
// const [totalGroups, setTotalGroups] = useState(0);
const updateGroups = (newGroups: Group[]) => {
setGroups(newGroups); // Trigger UI update if needed
setGroups(newGroups);
};
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(undefined);
@ -152,7 +160,6 @@ export default function Devices() {
})
}
const loadDevices = () => {
setDevicesLoading(true)
deviceAPI.list(
@ -359,14 +366,14 @@ export default function Devices() {
return(
<PageContainer padding={isMobile ? 0 : 2}>
<SmartBreadcrumb key={"Device page crumbs"}/>
{/* <SmartBreadcrumb /> */}
<Box
// onMouseEnter={() => setTabsHovered(true)}
// onMouseLeave={() => setTabsHovered(false)}
sx={{
borderRadius: 1,
border: "1px solid rgba(150, 150, 150, 0.2)",
marginTop: 1,
marginTop: 0,
marginBottom: 1
}}
>
@ -377,19 +384,19 @@ export default function Devices() {
scrollButtons="auto"
sx={{ borderRadius: 1 }}
>
<Tab value={"all"} label="All" />
<Tab wrapped value={"all"} label="All" />
{groupsLoading ?
<Tab value={"loading"} label={<CircularProgress />}/>
: groups.map((group, index) => {
if (group.id()) return (
<Tab value={group.id().toString()} label={group.name()} key={"group-tab-"+index}/>
<Tab className={classes.tab} wrapped value={group.id().toString()} label={<Typography variant="inherit" noWrap>{group.name()}</Typography>} key={"group-tab-"+index}/>
)
})}
<Tab value={"never"} label={<CreateGroupIcon className={classes.green} />} onClick={() => openGroupSettings(undefined, "add")} />
<Tab wrapped value={"never"} label={<CreateGroupIcon className={classes.green} />} onClick={() => openGroupSettings(undefined, "add")} />
</Tabs>
</Box>
<ResponsiveTable<pond.Device>
title="Devices"
title={<SmartBreadcrumb paddingBottom={1}/>}
subtitle={subtitle()}
rows={devices}
columns={columns()}

View file

@ -1,25 +1,230 @@
import { Grid2 } from "@mui/material";
import { Box, Chip, Grid2, Theme, Tooltip, Typography } from "@mui/material";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import { Group } from "models"
import { useState } from "react"
import { useLocation, useParams } from "react-router-dom";
import { Device, Group } from "models"
import { useEffect, useState } from "react"
import { useLocation, useNavigate, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useDeviceAPI, useGroupAPI } from "providers";
import GroupActions from "group/GroupActions";
import { pond } from "protobuf-ts/pond";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import { or } from "utils/types";
import { appendToUrl } from "navigation/Router";
import { getDeviceStateHelper } from "pbHelpers/DeviceState";
import { makeStyles } from "@mui/styles";
import { blue, green } from "@mui/material/colors";
import { permissionToString } from "pbHelpers/Permission";
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"]
},
cellContainer: {
margin: theme.spacing(1),
marginLeft: theme.spacing(2),
display: "flex",
// justifyContent: "center",
},
green: {
color: green["600"]
},
tab: {
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: theme.spacing(26),
whiteSpace: "nowrap",
padding: theme.spacing(1.5)
}
});
});
export default function GroupPage() {
const groupAPI = useGroupAPI()
const deviceAPI = useDeviceAPI()
const navigate = useNavigate()
const classes = useStyles()
const groupID = useParams<{ groupID: string }>()?.groupID ?? "";
const groupID = parseInt(useParams<{ groupID: string }>()?.groupID ?? "0");
const { state } = useLocation();
const [group, setGroup] = useState<Group>(state?.device ? Group.create(state.device) : Group.create())
const [group, setGroup] = useState<Group>(state?.group ? Group.create(state.group) : Group.create())
const [loadingGroup, setLoadingGroup] = useState(false)
const [loadingDevices, setLoadingDevices] = useState(false)
const [devices, setDevices] = useState<Device[]>([])
const [permissions, setPermissions] = useState<pond.Permission[]>([])
const [limit, setLimit] = useState(0)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [order, setOrder] = useState<"asc" | "desc">("asc")
const [orderBy, setOrderBy] = useState("")
const [search, setSearch] = useState("")
const loadGroup = () => {
setLoadingGroup(true)
groupAPI.getGroupAndPermissions(groupID).then(resp => {
if (resp.data.group) setGroup(Group.create(resp.data.group))
let newPerms: pond.Permission[] = []
resp.data.permissions.forEach(perm => {
if (typeof(perm) === "string") {
const permNumber = pond.Permission[perm as keyof typeof pond.Permission]
newPerms.push(permNumber)
} else {
newPerms.push(perm)
}
})
setPermissions(newPerms)
}).finally(() => {
setLoadingGroup(false)
})
}
useEffect(() => {
if (group.id() === groupID) return;
loadGroup()
}, [group, groupID])
const loadDevices = () => {
setLoadingDevices(true)
deviceAPI.list(
limit,
page*limit,
order,
orderBy,
search,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
getContextKeys(),
getContextTypes()
).then(resp => {
let newDevices: Device[] = [];
resp.data.devices.forEach(device => {
newDevices.push(Device.create(device))
})
setDevices(newDevices)
setTotal(resp.data.total)
}).finally(() => {
setLoadingDevices(false)
})
}
useEffect(() => {
loadDevices()
}, [group, limit, page, order, orderBy, search])
const toDevice = (device: Device) => {
navigate(appendToUrl(or(device.settings?.deviceId, "")), { state: {device: device} })
};
const columns = (): Column<Device>[] => {
return [
{
title: "State",
render: (device) => {
const status = device.status ?? pond.DeviceStatus.create()
const deviceStateHelper = getDeviceStateHelper(status.state);
return (
<Box className={classes.cellContainer}>
<Tooltip title={deviceStateHelper.description}>{deviceStateHelper.icon}</Tooltip>
</Box>
)
}
},
{
title: "ID",
render: device => <span className={classes.cellContainer}>
{device.settings?.deviceId}
</span>
},
{
title: "Device",
render: (device) => {
return (
<Box className={classes.cellContainer} >
<Chip
variant="outlined"
label={device.settings?.name ? device.settings.name : "Device " + device.settings?.deviceId}
/>
</Box>
)
}
},
{
title: "Description",
render: device => {
const description = device.settings?.description ?? ""
return (
<Box className={classes.cellContainer}>
<Tooltip title={device.settings?.description}>
<span>
{description.length > 20
? description.substring(0, 20) + "..."
: description}
</span>
</Tooltip>
</Box>
)
}
},
]
}
const handleChange = (event: any) => {
setLimit(event.target.value);
};
return (
<Grid2 container justifyContent={"space-between"}>
<Grid2>
<SmartBreadcrumb groupName={group.name()} />
<PageContainer>
<Grid2 container justifyContent={"space-between"} alignItems={"center"} paddingBottom={2}>
<Grid2 >
<SmartBreadcrumb groupName={group.name()} loading={loadingGroup} />
</Grid2>
<Grid2 marginTop={"auto"} marginBottom="auto">
<GroupActions
group={group}
permissions={permissions ? permissions : []}
devices={devices}
refreshCallback={() => {}}
/>
</Grid2>
</Grid2>
<Grid2>
{/* <GroupActions /> */}
</Grid2>
</Grid2>
<ResponsiveTable<Device>
title={group.name()}
subtitle={group.settings.description}
rows={devices}
columns={columns()}
total={total}
pageSize={limit}
page={page}
setPage={setPage}
handleRowsPerPageChange={handleChange}
onRowClick={toDevice}
setSearchText={setSearch}
isLoading={loadingDevices}
/>
</PageContainer>
)
}

View file

@ -9,6 +9,7 @@ import { useGroupAPI } from "providers";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { or } from "utils/types";
import PageContainer from "./PageContainer";
const useStyles = makeStyles((theme: Theme) => {
const isMobile = useMobile()
@ -115,18 +116,20 @@ export default function GroupsPage() {
}
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}
/>
<PageContainer >
<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}
/>
</PageContainer>
)
}

View file

@ -1,7 +1,8 @@
import { Container, SxProps, Theme } from "@mui/material";
import { makeStyles } from "@mui/styles";
import classNames from "classnames";
import React, { PropsWithChildren } from "react";
import { useMobile } from "hooks";
import { PropsWithChildren } from "react";
const useStyles = makeStyles((theme: Theme) => ({
pageContainer: {
@ -41,11 +42,13 @@ interface Props extends PropsWithChildren{
padding?: number;
}
export const PageContainer: React.FunctionComponent<Props> = props => {
export const PageContainer: React.FunctionComponent<Props> = (props: Props) => {
const classes = useStyles();
const { children, fullViewport, isCenterCenter, sx, padding } = props;
const isMobile = useMobile();
const { children, fullViewport, isCenterCenter, sx } = props;
// let fullViewport = false
// let isCenterCenter = false
const padding = props.padding ? props.padding : isMobile ? 1 : 2;
return (
<Container
className={classNames(
@ -58,7 +61,7 @@ export const PageContainer: React.FunctionComponent<Props> = props => {
}}
disableGutters
maxWidth={false}
children={<React.Fragment>{children}</React.Fragment>}
children={<>{children}</>}
/>
);
};

View file

@ -15,6 +15,7 @@ export interface IGroupAPIContext {
updateGroup: (id: number, group: pond.GroupSettings) => Promise<any>;
removeGroup: (id: number) => Promise<any>;
getGroup: (id: number) => Promise<any>;
getGroupAndPermissions: (id: number) => Promise<AxiosResponse<pond.GetGroupAndPermissionsResponse>>;
listGroups: (
limit: number,
offset: number,
@ -78,6 +79,27 @@ export default function GroupProvider(props: PropsWithChildren<Props>) {
return get(pondURL("/groups/" + id));
};
const getGroupAndPermissions = (
id: number,
keys?: string[],
types?: string[],
) => {
keys = keys ? keys : getContextKeys();
types = types? types : getContextTypes();
let url = "/groupAndPermissions/" + id
if (as) {
url = url + "?as=" + as +
+
(keys ? "?keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
} else {
url = url +
(keys ? "?keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
}
return get<pond.GetGroupAndPermissionsResponse>(pondURL(url));
};
const listGroups = (
limit: number,
offset: number,
@ -152,6 +174,7 @@ export default function GroupProvider(props: PropsWithChildren<Props>) {
updateGroup,
removeGroup,
getGroup,
getGroupAndPermissions,
listGroups,
listGroupDevices,
updateGroupPermissions

View file

@ -47,6 +47,7 @@ const useStyles = makeStyles((theme: Theme) => ({
titleBox: {
display: "flex",
alignItems: "center",
marginLeft: theme.spacing(1),
}
}))