diff --git a/src/group/GroupOverview.tsx b/src/group/GroupOverview.tsx new file mode 100644 index 0000000..641b7d2 --- /dev/null +++ b/src/group/GroupOverview.tsx @@ -0,0 +1,61 @@ +import { createStyles, List, ListItem, ListItemText, Theme } from "@mui/material"; +// import { Theme } from "@material-ui/core/styles/createMuiTheme"; +// import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; +import React from "react"; +import { Group } from "models"; +import { pond } from "protobuf-ts/pond"; +import { withStyles, WithStyles } from "@mui/styles"; + +const styles = (theme: Theme) => + createStyles({ + listDirectionRow: { + display: "flex", + flexDirection: "row", + padding: 0, + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis" + }, + listDirectionColumn: { + display: "flex", + flexDirection: "column", + padding: 0, + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis" + } + }); + +interface Props extends WithStyles { + group: Group; + devices?: pond.ComprehensiveDevice[]; + variant?: "row" | "column"; +} + +interface State {} + +class GroupOverview extends React.Component { + render() { + const { classes, variant, group, devices } = this.props; + const numDevices = devices?.length ? devices.length : 0; + return ( + + {numDevices > 0 && ( + + + + )} + {group.settings.description && ( + + + + )} + + ); + } +} + +export default withStyles(styles)(GroupOverview); diff --git a/src/user/AccessObject.tsx b/src/user/AccessObject.tsx new file mode 100644 index 0000000..2a5f32c --- /dev/null +++ b/src/user/AccessObject.tsx @@ -0,0 +1,474 @@ +import { + Avatar, + AppBar, + Box, + CircularProgress, + IconButton, + List, + ListItem, + ListItemText, + ListItemAvatar, + Toolbar, + Tooltip, + Typography, + ToggleButtonGroup, + ToggleButton +} from "@mui/material"; +import {Close as CloseIcon} from "@mui/icons-material"; +// import { ToggleButton, ToggleButtonGroup } from "@mui"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import SearchBar from "common/SearchBar"; +import { or } from "utils"; +import DeviceOverview from "device/DeviceOverview"; +import GroupOverview from "group/GroupOverview"; +import { useMobile, usePrevious, useSnackbar } from "hooks"; +import { Device, Group, Team, newScope } from "models"; +import { pond } from "protobuf-ts/pond"; +import { + useDeviceAPI, + useGlobalState, + useGroupAPI, + usePermissionAPI, + useTeamAPI, + useUserAPI +} from "providers"; +import { useCallback, useEffect, useState } from "react"; +import InfiniteScroll from "react-infinite-scroller"; +// import { useHistory } from "react-router"; +import { capitalize } from "utils/strings"; +import { SupervisedUserCircle as TeamIcon } from "@mui/icons-material"; +import { useNavigate } from "react-router-dom"; + +interface Props { + isOpen: boolean; + closeDialogCallback: () => void; +} + +const REQUEST_LIMIT = 25; + +export default function AccessObject(props: Props) { + const [{ user }, dispatch] = useGlobalState(); + // const history = useHistory(); + const navigate = useNavigate() + const isMobile = useMobile(); + const { error, info } = useSnackbar(); + const permissionAPI = usePermissionAPI(); + const deviceAPI = useDeviceAPI(); + const groupAPI = useGroupAPI(); + const teamAPI = useTeamAPI(); + const userAPI = useUserAPI(); + const { isOpen, closeDialogCallback } = props; + const prevOpen = usePrevious(isOpen); + const [kind, setKind] = useState("device"); + const prevKind = usePrevious(kind); + const [searchValue, setSearchValue] = useState(""); + const prevSearchValue = usePrevious(searchValue); + const [loading, setLoading] = useState(false); + const [devices, setDevices] = useState([]); + const [groups, setGroups] = useState([]); + const [teams, setTeams] = useState([]); + const [groupsOffset, setGroupsOffset] = useState(0); + const [groupsTotal, setGroupsTotal] = useState(0); + const [teamsOffset, setTeamsOffset] = useState(0); + const [teamsTotal, setTeamsTotal] = useState(0); + const [devicesOffset, setDevicesOffset] = useState(0); + const [devicesTotal, setDevicesTotal] = useState(0); + + const close = () => { + closeDialogCallback(); + }; + + const getAccess = (key: number | string, name: string, url: string, useImitation = false) => { + user.settings.useTeam = false; + dispatch({ key: "as", value: "" }); + userAPI.updateUser(user.id(), user.protobuf()).then(() => { + info("Will no longer view as team by default"); + permissionAPI + .shareObject( + newScope(kind, key.toString()), + user.settings.email, + [ + pond.Permission.PERMISSION_USERS, + pond.Permission.PERMISSION_READ, + pond.Permission.PERMISSION_WRITE, + pond.Permission.PERMISSION_SHARE + ], + useImitation + ) + .then((_response: any) => { + // history.push(url); + navigate(url) + close(); + }) + .catch((_err: any) => { + error("Failed to grant you access to " + name); + }); + }); + }; + + const load = useCallback(() => { + setLoading(true); + + kind === "group" + ? groupAPI + .listGroups(REQUEST_LIMIT, 0, "asc", undefined, searchValue, true) + .then((response: any) => { + let rGroupsOffset: number = response.data.nextOffset ? response.data.nextOffset : 0; + let rGroupsTotal: number = response.data.total ? response.data.total : 0; + let groups: Group[] = or(response.data.groups, []).map((g: any) => { + return Group.create(pond.Group.fromObject(g)); + }); + setGroups(groups); + setGroupsOffset(rGroupsOffset); + setGroupsTotal(rGroupsTotal); + }) + .catch((_error: any) => { + setGroups([]); + setGroupsOffset(0); + setGroupsTotal(0); + }) + .finally(() => setLoading(false)) + : kind === "team" + ? teamAPI + .listTeams(REQUEST_LIMIT, 0, "asc", undefined, searchValue, true) + .then(response => { + let rTeamsOffset: number = response.data.nextOffset ? response.data.nextOffset : 0; + let rTeamsTotal: number = response.data.total ? response.data.total : 0; + let teams: Team[] = or(response.data.teams, []).map((g: any) => { + return Team.create(pond.Team.fromObject(g)); + }); + setTeams(teams); + setTeamsOffset(rTeamsOffset); + setTeamsTotal(rTeamsTotal); + }) + .finally(() => setLoading(false)) + : deviceAPI + .list( + REQUEST_LIMIT, + 0, + "asc", + undefined, + searchValue, + undefined, + undefined, + undefined, + true + ) + .then((response: any) => { + let rDevicesOffset: number = response.data.nextOffset ? response.data.nextOffset : 0; + let rDevicesTotal: number = response.data.total ? response.data.total : 0; + let devices: Device[] = or(response.data.devices, []).map((d: any) => { + return Device.create(pond.Device.fromObject(d)); + }); + setDevices(devices); + setDevicesTotal(rDevicesTotal); + setDevicesOffset(rDevicesOffset); + }) + .catch((_error: any) => { + setDevices([]); + setDevicesOffset(0); + setDevicesTotal(0); + }) + .finally(() => setLoading(false)); + }, [deviceAPI, groupAPI, teamAPI, kind, searchValue]); + + const loadMoreDevices = useCallback(() => { + deviceAPI + .list( + REQUEST_LIMIT, + devicesOffset, + "asc", + undefined, + searchValue, + undefined, + undefined, + undefined, + true + ) + .then((response: any) => { + let rDevicesOffset: number = response.data.nextOffset ? response.data.nextOffset : 0; + let rDevicesTotal: number = response.data.total ? response.data.total : 0; + let moreDevices: Device[] = or(response.data.devices, []).map((d: any) => { + return Device.create(pond.Device.fromObject(d)); + }); + setDevices([...new Set([...devices, ...moreDevices])]); + setDevicesTotal(rDevicesTotal); + setDevicesOffset(rDevicesOffset); + }) + .catch((_error: any) => { + setDevices([]); + setDevicesOffset(0); + setDevicesTotal(0); + }); + }, [deviceAPI, devicesOffset, searchValue, devices]); + + const loadMoreGroups = useCallback(() => { + groupAPI + .listGroups(REQUEST_LIMIT, groupsOffset, "asc", undefined, searchValue, true) + .then((response: any) => { + let rGroupsOffset: number = response.data.nextOffset ? response.data.nextOffset : 0; + let rGroupsTotal: number = response.data.total ? response.data.total : 0; + let moreGroups: Group[] = or(response.data.groups, []).map((g: any) => { + return Group.create(pond.Group.fromObject(g)); + }); + setGroups([...new Set([...groups, ...moreGroups])]); + setGroupsOffset(rGroupsOffset); + setGroupsTotal(rGroupsTotal); + }) + .catch((_error: any) => { + setGroups([]); + setGroupsOffset(0); + setGroupsTotal(0); + }); + }, [groupAPI, groupsOffset, searchValue, groups]); + + const loadMoreTeams = useCallback(() => { + teamAPI + .listTeams(REQUEST_LIMIT, teamsOffset, "asc", undefined, searchValue, true) + .then((response: any) => { + let rTeamsOffset: number = response.data.nextOffset ? response.data.nextOffset : 0; + let rTeamsTotal: number = response.data.total ? response.data.total : 0; + let moreTeams: Team[] = or(response.data.teams, []).map((g: any) => { + return Team.create(pond.Team.fromObject(g)); + }); + setTeams([...new Set([...teams, ...moreTeams])]); + setTeamsOffset(rTeamsOffset); + setTeamsTotal(rTeamsTotal); + }) + .catch((_error: any) => { + setTeams([]); + setTeamsOffset(0); + setTeamsTotal(0); + }); + }, [teamAPI, teamsOffset, searchValue, teams]); + + useEffect(() => { + if (isOpen && (prevSearchValue !== searchValue || prevKind !== kind || isOpen !== prevOpen)) { + load(); + } + }, [ + prevSearchValue, + searchValue, + load, + kind, + prevKind, + loadMoreDevices, + loadMoreGroups, + isOpen, + prevOpen + ]); + + const groupURL = (group: Group) => { + return "/groups/" + group.id().toString(); + }; + + const groupItem = (g: Group) => { + return ( + getAccess(g.id(), g.name(), groupURL(g))}> + + {g.name()} + + } + secondaryTypographyProps={{ component: "span" }} + secondary={} + /> + + ); + }; + + const deviceURL = (device: Device) => { + return "/devices/" + device.id().toString(); + }; + + const teamURL = (team: Team) => { + return "/teams/" + team.key(); + }; + + const deviceItem = (d: Device) => { + return ( + getAccess(d.id(), d.name(), deviceURL(d))}> + + {d.name()} + + } + secondaryTypographyProps={{ component: "span" }} + secondary={} + /> + + ); + }; + + const teamItem = (d: Team) => { + return ( + getAccess(d.key(), d.name(), teamURL(d), false)}> + + + {/* Draw the team icon id src icon errors */} + + + + + {d.name()} + + } + secondaryTypographyProps={{ component: "span" }} + secondary={d.settings.info} + /> + + ); + }; + + return ( + + + + + + + + Access {capitalize(kind)} + + + newKind !== null && setKind(newKind)} + aria-label="object-kind"> + + Devices + + + Groups + + + Teams + + + + {!isMobile && ( + + setSearchValue(value)} /> + + )} + + {isMobile && ( + + + setSearchValue(value)} /> + + + )} + + + {loading ? ( + + + + ) : kind === "group" ? ( + loadMoreGroups()} + hasMore={groups.length < groupsTotal} + useWindow={false} + loader={ + + + + }> + + {groups.length === 0 ? ( + + + No groups found + + + ) : ( + groups.map(g => groupItem(g)) + )} + + + ) : kind === "team" ? ( + loadMoreTeams()} + hasMore={teams.length < teamsTotal} + useWindow={false} + loader={ + + + + }> + + {teams.length === 0 ? ( + + + No teams found + + + ) : ( + teams.map(t => teamItem(t)) + )} + + + ) : ( + loadMoreDevices()} + hasMore={devices.length < devicesTotal} + useWindow={false} + loader={ + + + + }> + + {devices.length === 0 ? ( + + + No devices found + + + ) : ( + devices.map(d => deviceItem(d)) + )} + + + )} + + ); +}