diff --git a/package-lock.json b/package-lock.json index 1020b78..84c6372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4247,7 +4247,7 @@ }, "node_modules/protobuf-ts": { "version": "1.0.0", - "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#91931b1936a92bb0bc24700664adf72eb8a912dd", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#3317c70ecda93740f8552e656429a42f1b15f350", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/src/common/CircleGraphIcon.tsx b/src/common/CircleGraphIcon.tsx new file mode 100644 index 0000000..405ed22 --- /dev/null +++ b/src/common/CircleGraphIcon.tsx @@ -0,0 +1,83 @@ +import { + Box, + CircularProgress, + SxProps, + Theme, + useTheme +} from "@mui/material"; +import React, { ReactElement } from "react"; + +interface Props { + size?: number | string; + progress?: number; + // color?: OverridableStringUnion< + // 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' | 'inherit', + // CircularProgressPropsColorOverrides + // >; + color?: string; + sx?: SxProps, + icon: ReactElement, +} + +export default function CircleGraphIcon(props: Props) { + + const { progress, icon } = props; + const theme = useTheme() + const size = props.size ? props.size : theme.spacing(5) + const exists = props.color ? props.color in theme.palette : false + + const color = exists && props.color ? (theme.palette as any)[props.color].main : props.color + console.log(props.color) + console.log(color) + + return ( + + {/* Circular Graph */} + + + {/* Icon at the center */} + + {/* CLone element to apply color and size */} + {icon && + React.cloneElement(icon, { + // color: {color}, + sx: { + width: `calc(${size} - 14px)`, + height: `calc(${size} - 14px)`, + color: {color}, + }, + })} + + + ) +} \ No newline at end of file diff --git a/src/device/DeviceTags.tsx b/src/device/DeviceTags.tsx index 9b65808..83b78a0 100644 --- a/src/device/DeviceTags.tsx +++ b/src/device/DeviceTags.tsx @@ -1,5 +1,4 @@ import { - CircularProgress, Dialog, DialogContent, DialogTitle, @@ -20,9 +19,8 @@ import TagSettings from "common/TagSettings"; import { Device, Tag } from "models"; import { filterByTag } from "pbHelpers/Tag"; import { useDeviceAPI, useSnackbar, useTagAPI } from "providers"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useEffect, useState } from "react"; import { pond } from "protobuf-ts/pond"; -import { getContextKeys, getContextTypes } from "pbHelpers/Context"; const useStyles = makeStyles((_theme: Theme) => { return ({ @@ -137,14 +135,14 @@ export default function DeviceTags(props: DeviceTagsProps) { // const [{ tags }] = useGlobalState(); const tagAPI = useTagAPI() const [tags, setTags] = useState([]) - const [loading, setLoading] = useState(false) + // const [loading, setLoading] = useState(false) const deviceAPI = useDeviceAPI(); const { error } = useSnackbar(); const [deviceTags, setDeviceTags] = useState(device.status.tagKeys); - const previousDeviceRef = useRef(device); + // const previousDeviceRef = useRef(device); const loadTags = () => { - setLoading(true) + // setLoading(true) tagAPI.listTags().then(resp => { let newTags: Tag[] = []; resp.data.tags.forEach((tag: pond.Tag) => { @@ -152,12 +150,11 @@ export default function DeviceTags(props: DeviceTagsProps) { }) setTags(newTags) }).finally(() => { - setLoading(false) + // setLoading(false) }) } useEffect(() => { - console.log(props.tags) let newTags: string[] = [] props.tags.forEach(tag => { if (tag.settings) newTags.push(tag.settings?.key) @@ -169,34 +166,6 @@ export default function DeviceTags(props: DeviceTagsProps) { loadTags() }, []) - // useEffect(() => { - // // if (previousDeviceRef.current !== device) { - // let keys = getContextKeys() - // keys.push(device.id().toString()) - // let types = getContextTypes() - // types.push("device") - // setDeviceTags(device.status.tagKeys); - // previousDeviceRef.current = device; - // tagAPI.listTags( - // undefined, - // undefined, - // undefined, - // undefined, - // undefined, - // undefined, - // keys, - // types, - // ).then(resp => { - // // console.log(resp.data) - // let newTagKeys: string[] = [] - // resp.data.tags.forEach((tag: any) => { - // newTagKeys.push(tag.settings.key) - // }) - // setDeviceTags(newTagKeys) - // }) - // // } - // }, [device]); - const addTag = (tag: Tag) => { if (!deviceTags.some(dt => dt === tag.key())) { deviceAPI diff --git a/src/device/DevicesSummary.tsx b/src/device/DevicesSummary.tsx new file mode 100644 index 0000000..69531b4 --- /dev/null +++ b/src/device/DevicesSummary.tsx @@ -0,0 +1,140 @@ +import { + AutoAwesomeMosaic, + Battery20, + Check, + CheckCircle, + PermScanWifi, +} from "@mui/icons-material"; +import { Card, Grid2, Typography, useTheme } from "@mui/material"; +import { blue, green, purple, red } from "@mui/material/colors"; +import CircleGraphIcon from "common/CircleGraphIcon"; +import { getContextKeys, getContextTypes } from "pbHelpers/Context"; +import { pond } from "protobuf-ts/pond"; +import { useDeviceAPI } from "providers"; +import { useEffect, useState } from "react"; + +interface Props {} + +export default function DevicesSummary(props: Props) { + + const deviceAPI = useDeviceAPI() + const theme = useTheme() + + const [stats, setStats] = useState(pond.DeviceStatistics.create()) + const [loadingStats, setLoadingStats] = useState(false) + + const loadDeviceStatistics = () => { + setLoadingStats(true) + deviceAPI.statistics( + getContextKeys(), + getContextTypes() + ).then(resp => { + let stats = pond.DeviceStatistics.fromObject(resp.data.stats) + setStats(stats) + }).finally(() => { + setLoadingStats(false) + }) + } + + useEffect(() => { + loadDeviceStatistics() + }, []) + + const getPercent = (devices: number) => { + const ratio = devices/stats.total + return ratio*100 + } + + return ( + + + + + + } + /> + + + + {stats.total} Devices + + + Total + + + + + + + + + + } + /> + + + + {stats.active} Devices + + + Active + + + + + + + + + + } + /> + + + + {stats.warning} Devices + + + Low Power + + + + + + + + + + } + /> + + + + {stats.offline} Devices + + + Offline + + + + + + + ) +} \ No newline at end of file diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 63a8c8c..43d18bf 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -1,4 +1,4 @@ -import { ChevronRight, People, PeopleOutline, PeopleOutlined, PeopleSharp, Person } from "@mui/icons-material"; +import { ChevronRight, People, Person } from "@mui/icons-material"; import ChevronLeft from "@mui/icons-material/ChevronLeft"; import { darken, Divider, Grid2 as Grid, IconButton, lighten, List, ListItemButton, ListItemIcon, ListItemText, SwipeableDrawer, Theme, Toolbar, Tooltip, useTheme } from "@mui/material"; import classNames from "classnames"; @@ -6,7 +6,6 @@ import { useWidth } from "../hooks/useWidth"; import { makeStyles } from "@mui/styles"; import BindaptIcon from "../products/Bindapt/BindaptIcon"; import { Link, useLocation } from "react-router-dom"; -import { getSignatureAccentColour } from "services/whiteLabel"; const drawerWidth = 230; diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 482cce5..b5a3978 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -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 { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon, CheckCircle } from "@mui/icons-material"; +import { Box, Card, Chip, CircularProgress, Grid2, 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"; @@ -16,6 +16,8 @@ import { Device, Group } from "models"; import GroupSettings from "group/GroupSettings"; import SmartBreadcrumb from "common/SmartBreadcrumb"; import GroupActions from "group/GroupActions"; +import CircleGraphIcon from "common/CircleGraphIcon"; +import DevicesSummary from "device/DevicesSummary"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -74,6 +76,8 @@ export default function Devices() { const [total, setTotal] = useState(0); const [devices, setDevices] = useState([]) const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); + const [stats, setStats] = useState(pond.DeviceStatistics.create()) + const [loadingStats, setLoadingStats] = useState(false) const [groupsLoading, setGroupsLoading] = useState(false) const [groups, setGroups] = useState([]); @@ -190,10 +194,28 @@ export default function Devices() { }) } + const loadDeviceStatistics = () => { + setLoadingStats(true) + deviceAPI.statistics( + getContextKeys(), + getContextTypes() + ).then(resp => { + let stats = pond.DeviceStatistics.fromObject(resp.data.stats) + // console.log(stats) + setStats(stats) + }).finally(() => { + setLoadingStats(false) + }) + } + useEffect(() => { loadDevices() }, [limit, page, order, orderBy, search, tab]) + useEffect(() => { + loadDeviceStatistics() + }, []) + useEffect(() => { // console.log("groups loaded") loadGroups() @@ -313,78 +335,9 @@ export default function Devices() { return group } - // interface MyTabButtonProps { - // onClick?: React.MouseEventHandler; - // disabled?: boolean; - // } - // const [leftButtonProps, setLeftButtonProps] = useState({disabled: true}) - // const [rightButtonProps, setRightButtonProps] = useState({disabled: true}) - // Use refs to store the props received during render - // const leftButtonRef = useRef({}); - // const rightButtonRef = useRef({}); - // 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( - {/* */} setTabsHovered(true)} - // onMouseLeave={() => setTabsHovered(false)} sx={{ borderRadius: 1, border: "1px solid rgba(150, 150, 150, 0.2)", @@ -410,6 +363,7 @@ export default function Devices() { } onClick={() => openGroupSettings(undefined, "add")} /> + title={} subtitle={subtitle()} diff --git a/src/products/Bindapt/BindaptIcon.tsx b/src/products/Bindapt/BindaptIcon.tsx index d43814c..fd0cf0b 100644 --- a/src/products/Bindapt/BindaptIcon.tsx +++ b/src/products/Bindapt/BindaptIcon.tsx @@ -1,7 +1,6 @@ import BindaptDarkIcon from "../../assets/products/bindapt/bindaptPlusDark.png"; import BindaptLightIcon from "../../assets/products/bindapt/bindaptPlusLight.png"; import { ImgIcon } from "../../common/ImgIcon"; -import React from "react"; import { useThemeType } from "../../hooks/useThemeType"; interface Props { diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index d6f9212..4db910a 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -100,6 +100,10 @@ export interface IDeviceAPIContext { keys?: string[], types?: string[] ) => Promise; + statistics: ( + keys?: string[], + types?: string[] + ) => Promise; tag: (id: number, tag: string) => Promise; untag: (id: number, tag: string) => Promise; getDatacap: (id: number) => Promise; @@ -177,6 +181,22 @@ export default function DeviceProvider(props: PropsWithChildren) { return get(url); }; + const statistics = ( + keys?: string[], + types?: string[] + ) => { + if (types && types.length > 0 && types[0] === "team") + return get( + pondURL( + "/deviceStatistics/" + + (keys ? "?keys=" + keys.toString() : "") + + (types ? "&types=" + types.toString() : "") + ) + ); + const url = pondURL("/deviceStatistics"); + return get(url); + }; + const getDevicePageData = ( id: number | string, keys?: string[], @@ -615,6 +635,7 @@ export default function DeviceProvider(props: PropsWithChildren) { update, remove, get: getDevice, + statistics, getPageData: getDevicePageData, getMulti, getGeoJson: getDeviceGeoJSON,