diff --git a/src/common/DeviceActions.tsx b/src/common/DeviceActions.tsx new file mode 100644 index 0000000..4780743 --- /dev/null +++ b/src/common/DeviceActions.tsx @@ -0,0 +1,537 @@ +import { + Divider, + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Theme, + Tooltip +} from "@mui/material"; +// import { amber, blue, green } from "@mui/co"; +import { + AddCircle as AddIcon, + ExitToApp as RemoveSelfIcon, + History as HistoryIcon, + MoreVert as MoreIcon, + PauseCircleFilled, + PlayCircleFilled, + Reorder as ComponentOrderIcon, + Save as SaveDeviceProfileIcon, + SaveAlt as LoadDeviceProfileIcon, + Settings as DeviceSettingsIcon, + Share as ShareObjectIcon, + AccountCircle as ObjectUsersIcon, + SupervisedUserCircle as ObjectTeamsIcon, + Sync as SyncDeviceIcon, + Wifi as WifiIcon +} from "@mui/icons-material"; +import { Skeleton } from "@mui/material"; +import Datadog from "assets/external/datadog.png"; +import { ImgIcon } from "common/ImgIcon"; +import NotificationButton from "common/NotificationButton"; +// import ComponentOrder from "component/ComponentOrder"; +// import ComponentSettings from "component/ComponentSettings"; +// import DeviceSettings from "device/DeviceSettings"; +// import LoadDeviceProfile from "device/LoadDeviceProfile"; +// import { PauseData } from "device/PauseData"; +// import { ResumeData } from "device/ResumeData"; +// import SaveDeviceProfile from "device/SaveDeviceProfile"; +// import SyncDevice from "device/SyncDevice"; +// import UpgradeDevice from "device/UpgradeDevice"; +// import InteractionSettings from "interactions/InteractionSettings"; +import { cloneDeep } from "lodash"; +// import { Component, Device, deviceScope, Interaction } from "models"; +import { Device, deviceScope } from "models"; +// import { MatchParams } from "navigation/Routes"; +import { isShareableLink } from "pbHelpers/Device"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers"; +import React, { useState } from "react"; +// import { useHistory, useRouteMatch } from "react-router"; +import { isOffline } from "utils/environment"; +import { or } from "utils/types"; +import ObjectUsers from "user/ObjectUsers"; +import RemoveSelfFromObject from "user/RemoveSelfFromObject"; +import ShareObject from "user/ShareObject"; +// import Connection from "./Connection"; +// import ObjectTeams from "teams/ObjectTeams"; +// import ArcGISDeviceData from "./ArcGISDeviceData"; +// import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability"; +import { amber, blue, green } from "@mui/material/colors"; +import { makeStyles } from "@mui/styles"; +import { useNavigate } from "react-router-dom"; + +const useStyles = makeStyles((theme: Theme) => { + // const isMobile = useMobile() + return ({ + greenIcon: { + color: green["500"], + "&:hover": { + color: green["600"] + } + }, + amberIcon: { + color: amber["600"], + "&:hover": { + color: amber["700"] + } + }, + blueIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + red: { + color: "var(--status-alert)" + } + }); +}); + +interface Props { + device: Device; + isPaused: boolean; + // components: Component[]; + // interactions: Interaction[]; + refreshCallback: () => void; + // availablePositions: DeviceAvailabilityMap; + // availableOffsets: OffsetAvailabilityMap; + permissions: Array; + preferences: pond.DevicePreferences; + isLoading: boolean; + toggleNotificationPreference: Function; +} + +interface DialogState { + isAddComponentDialogOpen: boolean; + isDeviceSettingsDialogOpen: boolean; + isShareObjectDialogOpen: boolean; + isSyncDeviceDialogOpen: boolean; + isPauseDialogOpen: boolean; + isResumeDialogOpen: boolean; + isInteractionSettingsOpen: boolean; + isObjectUsersDialogOpen: boolean; + isObjectTeamsDialogOpen: boolean; + isRemoveSelfDialogOpen: boolean; + isSaveDeviceProfileOpen: boolean; + isLoadDeviceProfileOpen: boolean; + isConnectionDialogOpen: boolean; + isComponentOrderDialogOpen: boolean; + isJsonDataDialogOpen: boolean; +} + +export default function DeviceActions(props: Props) { + const { + device, + isPaused, + // components, + // interactions, + refreshCallback, + // availablePositions, + // availableOffsets, + permissions, + preferences, + isLoading, + toggleNotificationPreference + } = props; + const classes = useStyles(); + const [{ user }] = useGlobalState(); + const canWrite = permissions.includes(pond.Permission.PERMISSION_WRITE); + const navigate = useNavigate(); + // const match = useRouteMatch(); + const [menuAnchorEl, setMenuAnchorEl] = useState(null); + const [dialogState, setDialogState] = useState({ + isAddComponentDialogOpen: false, + isDeviceSettingsDialogOpen: false, + isShareObjectDialogOpen: false, + isSyncDeviceDialogOpen: false, + isPauseDialogOpen: false, + isResumeDialogOpen: false, + isInteractionSettingsOpen: false, + isObjectUsersDialogOpen: false, + isObjectTeamsDialogOpen: false, + isRemoveSelfDialogOpen: false, + isSaveDeviceProfileOpen: false, + isLoadDeviceProfileOpen: false, + isConnectionDialogOpen: false, + isComponentOrderDialogOpen: false, + isJsonDataDialogOpen: false + }); + + const openDialog = (target: keyof DialogState) => { + let updatedDialogState = cloneDeep(dialogState); + updatedDialogState[target] = true; + setDialogState(updatedDialogState); + setMenuAnchorEl(null); + }; + + const closeDialog = (target: keyof DialogState) => { + let updatedDialogState = cloneDeep(dialogState); + updatedDialogState[target] = false; + setDialogState(updatedDialogState); + }; + + const closeAndRefresh = (target: keyof DialogState) => (refresh: boolean) => { + closeDialog(target); + if (refresh) props.refreshCallback(); + }; + + const deviceMenu = () => { + const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE); + const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS); + const canProvision = user.allowedTo("provision"); + const isCellular = device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON; + const canPause = canWrite && isCellular && user.allowedTo("pause-data"); + const isWifi = device.settings.platform === pond.DevicePlatform.DEVICE_PLATFORM_PHOTON; + // const isShareLink = isShareableLink(match.params.deviceID); + const isShareLink = false + + return ( + setMenuAnchorEl(null)} + disableAutoFocusItem> + {canWrite && ( + { + openDialog("isAddComponentDialogOpen"); + }} + dense> + + + + + + )} + {canWrite && ( + openDialog("isInteractionSettingsOpen")} dense> + + + + + + )} + {canWrite && } + + {!isOffline() && canShare && ( + openDialog("isShareObjectDialogOpen")} dense> + + + + + + )} + {canWrite && + user.hasAdmin() && ( //TODO: once the resync issue has been resolved remove the admin check + openDialog("isSyncDeviceDialogOpen")} dense> + + + + + + )} + {user.hasFeature("json") && ( + openDialog("isJsonDataDialogOpen")} dense> + + + + + + )} + openDialog("isComponentOrderDialogOpen")} dense> + + + + + + + + {canPause && !isPaused && ( + openDialog("isPauseDialogOpen")} dense> + + + + + + )} + {canPause && isPaused && ( + openDialog("isResumeDialogOpen")} dense> + + + + + + )} + {isWifi && canWrite && ( + openDialog("isConnectionDialogOpen")} dense> + + + + + + )} + {(canPause || (isWifi && canWrite)) && } + {canProvision && ( + openDialog("isSaveDeviceProfileOpen")} dense> + + + + + + )} + {canProvision && ( + openDialog("isLoadDeviceProfileOpen")} dense> + + + + + + )} + {canProvision && } + {!isOffline() && canManageUsers && ( + openDialog("isObjectUsersDialogOpen")} dense> + + + + + + )} + {!isOffline() && canManageUsers && ( + openDialog("isObjectTeamsDialogOpen")} dense> + + + + + + )} + { + // const groupID: number = parseInt(match.params.groupID, 10); + // const groupPath: string = groupID > 0 ? "/groups/" + groupID.toString() : ""; + const devicePath: string = "/devices/" + device.id().toString(); + // let path = groupPath + devicePath + "/history"; + let path = devicePath + "/history"; + navigate(path); + }} + dense> + + + + + + {/* {canProvision && ( + { + window.open( + "https://app.datadoghq.com/logs?cols=core_host%2Ccore_service&index=main&live=true&messageDisplay=inline&stream_sort=desc&query=%40device%3A" + + device.id(), + "_blank" + ); + }} + dense> + + + + + + )} */} + + {!isShareLink && ( + openDialog("isRemoveSelfDialogOpen")} dense> + + + + + + )} + + ); + }; + + const dialogs = () => { + const deviceName = device.name(); + const { + isAddComponentDialogOpen, + isDeviceSettingsDialogOpen, + isShareObjectDialogOpen, + isSyncDeviceDialogOpen, + isPauseDialogOpen, + isResumeDialogOpen, + isInteractionSettingsOpen, + isObjectUsersDialogOpen, + isObjectTeamsDialogOpen, + isRemoveSelfDialogOpen, + isSaveDeviceProfileOpen, + isLoadDeviceProfileOpen, + isConnectionDialogOpen, + isComponentOrderDialogOpen, + isJsonDataDialogOpen + } = dialogState; + + return ( + + {/* closeDialog("isDeviceSettingsDialogOpen")} + refreshCallback={refreshCallback} + canEdit={canWrite} + components={components} + /> */} + {/* closeDialog("isAddComponentDialogOpen")} + availablePositions={availablePositions} + availableOffsets={availableOffsets} + refreshCallback={refreshCallback} + canEdit={canWrite} + /> + closeDialog("isSyncDeviceDialogOpen")} + refreshCallback={refreshCallback} + /> */} + closeDialog("isShareObjectDialogOpen")} + /> + {/* closeDialog("isInteractionSettingsOpen")} + refreshCallback={refreshCallback} + canEdit={canWrite} + /> */} + closeDialog("isObjectUsersDialogOpen")} + refreshCallback={refreshCallback} + /> + {/* closeDialog("isObjectTeamsDialogOpen")} + refreshCallback={refreshCallback} + /> */} + closeDialog("isRemoveSelfDialogOpen")} + /> + {/* closeDialog("isSaveDeviceProfileOpen")} + device={device} + components={components} + interactions={interactions} + tagIds={device.status.tagKeys} + refreshCallback={refreshCallback} + /> */} + {/* closeDialog("isLoadDeviceProfileOpen")} + device={device} + refreshCallback={refreshCallback} + /> */} + {/* + + + + */} + + ); + }; + + const buttons = () => { + return ( + + {/* {canWrite && user.allowedTo("provision") && } */} + {/* {!isShareableLink(match.params.deviceID) && ( */} + + ); + }; + + if (isLoading) + return ( + + + + + + ); + + return ( + + {buttons()} + + {deviceMenu()} + {dialogs()} + + ); +} diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 24732c6..d9bdd05 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -5,7 +5,7 @@ import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ProvisionDevice from "device/ProvisionDevice"; import { pond } from "protobuf-ts/pond"; -import { useDeviceAPI } from "providers"; +import { useDeviceAPI, useGroupAPI } from "providers"; import { useEffect, useState } from "react"; import PageContainer from "./PageContainer"; import { useMobile } from "hooks"; @@ -34,6 +34,7 @@ export default function Devices() { const navigate = useNavigate(); const location = useLocation(); const deviceAPI = useDeviceAPI(); + const groupAPI = useGroupAPI(); const [limit, setLimit] = useState(10); const [page, setPage] = useState(0); const [order, ] = useState<"asc" | "desc">("asc"); @@ -43,6 +44,13 @@ export default function Devices() { const [devices, setDevices] = useState([]) const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); + const [groupLimit, setGroupLimit] = useState(10); + const [groupPage, setGroupPage] = useState(0); + const [orderGroup, ] = useState<"asc" | "desc">("asc"); + const [orderGroupBy, ] = useState("name"); + const [searchGroup, setGroupSearch] = useState(""); + const [totalGroups, setTotalGroups] = useState(0); + const openProvisionDialog = () => { setIsProvisionDialogOpen(true); }; @@ -51,6 +59,18 @@ export default function Devices() { setIsProvisionDialogOpen(false); }; + const loadGroups = () => { + groupAPI.listGroups( + groupLimit, + groupPage*groupLimit, + orderGroup, + orderGroupBy, + searchGroup, + ).then(resp => { + console.log(resp.data) + }) + } + const loadDevices = () => { deviceAPI.list( limit, @@ -76,12 +96,17 @@ export default function Devices() { loadDevices() }, [limit, page, order, orderBy, search]) + useEffect(() => { + loadDevices() + }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup]) + + const handleChange = (event: any) => { setLimit(event.target.value); }; const appendToUrl = (appendage: number | string) => { - const basePath = location.pathname.replace(/\/$/, ""); // Ensure no trailing slash + const basePath = location.pathname.replace(/\/$/, ""); return(`${basePath}/${appendage}`); }; diff --git a/src/providers/index.ts b/src/providers/index.ts index 5e3403b..8c485cb 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -15,7 +15,7 @@ export { // useFieldAPI, // useFieldMarkerAPI, // useFirmwareAPI, - // useGroupAPI, + useGroupAPI, // useHarvestPlanAPI, // useHarvestYearAPI, // useHomeMarkerAPI, diff --git a/src/providers/pond/groupAPI.tsx b/src/providers/pond/groupAPI.tsx new file mode 100644 index 0000000..6830071 --- /dev/null +++ b/src/providers/pond/groupAPI.tsx @@ -0,0 +1,154 @@ +import { useHTTP, usePermissionAPI } from "hooks"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; +import { User, groupScope } from "models"; +import { pondURL } from "./pond"; +import { AxiosResponse } from "axios"; +import { useGlobalState } from "providers"; +import { getContextKeys, getContextTypes } from "pbHelpers/Context"; + +export interface IGroupAPIContext { + addGroup: (group: pond.GroupSettings) => Promise; + addDevice: (group: number, device: number) => Promise; + removeDevice: (group: number, device: number) => Promise; + updateGroup: (id: number, group: pond.GroupSettings) => Promise; + removeGroup: (id: number) => Promise; + getGroup: (id: number) => Promise; + listGroups: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean + ) => Promise; + listGroupDevices: ( + id: number, + limit: number, + offset: number, + order: "asc" | "desc", + search?: string, + comprehensive?: boolean + ) => Promise>; + updateGroupPermissions: (id: number, users: User[]) => Promise; +} + +export const GroupAPIContext = createContext({} as IGroupAPIContext); + +interface Props {} + +export default function GroupProvider(props: PropsWithChildren) { + const { children } = props; + const { get, post, put, del } = useHTTP(); + const permissionAPI = usePermissionAPI(); + const [{ as }] = useGlobalState(); + + const addGroup = (group: pond.GroupSettings) => { + if (as) return post(pondURL("/groups?as=" + as), group); + return post(pondURL("/groups"), group); + }; + + const addDevice = (group: number, device: number) => { + let url = "/groups/" + group + "/devices/" + device + "/add"; + if (as) return post(pondURL(url + as), group); + return post(pondURL(url), group); + }; + + const removeDevice = (group: number, device: number) => { + let url = "/groups/" + group + "/devices/" + device + "/remove"; + if (as) return post(pondURL(url + as), group); + return post(pondURL(url), group); + }; + + const updateGroup = (id: number, group: pond.GroupSettings) => { + if (as) return put(pondURL("/groups/" + id + "?as=" + as), group); + return put(pondURL("/groups/" + id), group); + }; + + const removeGroup = (id: number) => { + if (as) return del(pondURL("/groups/" + id + "?as=" + as)); + return del(pondURL("/groups/" + id)); + }; + + const getGroup = (id: number) => { + if (as) return get(pondURL("/groups/" + id + "?as=" + as)); + return get(pondURL("/groups/" + id)); + }; + + const listGroups = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + asRoot?: boolean + ) => { + let keys = getContextKeys() + let types = getContextTypes() + return get( + pondURL( + "/groups" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&orderBy=" + or(orderBy, "id")) + + (search ? "&search=" + search : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (as ? "&as=" + as.toString() : "") + + (keys.length > 0 ? "&keys=" + keys : "") + + (types.length > 0 ? "&types=" + types : "") + ) + ); + }; + + const listGroupDevices = ( + id: number, + limit: number, + offset: number, + order: "asc" | "desc", + search?: string, + comprehensive: boolean = false + ) => { + return get( + pondURL( + "/groups/" + + id + + "/devices?limit=" + + limit + + "&offset=" + + offset + + "&order=" + + order + + (search ? "&search=" + search : "") + + (comprehensive ? "&comprehensive=" + comprehensive.toString() : "") + + (as ? "&as=" + as.toString() : "") + ) + ); + }; + + const updateGroupPermissions = (id: number, users: User[]) => { + return permissionAPI.updatePermissions(groupScope(id.toString()), users); + }; + + return ( + + {children} + + ); +} + +export const useGroupAPI = () => useContext(GroupAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index 4301791..0272de3 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -9,6 +9,7 @@ import GateProvider, { useGateAPI } from "./gateAPI"; import NoteProvider, { useNoteAPI } from "./noteAPI"; import DeviceProvider, { useDeviceAPI } from "./deviceAPI"; import BackpackProvider, { useBackpackAPI } from "./backpackAPI"; +import GroupProvider, { useGroupAPI } from "./groupAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -33,9 +34,11 @@ export default function PondProvider(props: PropsWithChildren) { - - {children} - + + + {children} + + @@ -55,5 +58,6 @@ export { usePermissionAPI, useBinAPI, useGateAPI, + useGroupAPI, useNoteAPI, };