implemented device statistics

This commit is contained in:
Carter 2025-01-27 10:36:54 -06:00
parent 0076f2d208
commit 3a000b0094
8 changed files with 276 additions and 111 deletions

View file

@ -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<Theme>,
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 (
<Box
sx={{
position: 'relative',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
width: size,
height: size,
marginBottom: theme.spacing(-1),
...props.sx,
}}
>
{/* Circular Graph */}
<CircularProgress
variant="determinate"
value={progress}
// color={color}
thickness={4}
style={{
width: size,
height: size,
position: 'absolute',
}}
sx={{ color: {color} }}
/>
{/* Icon at the center */}
<Box
sx={{
position: 'absolute',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: {color}
}}
>
{/* CLone element to apply color and size */}
{icon &&
React.cloneElement(icon, {
// color: {color},
sx: {
width: `calc(${size} - 14px)`,
height: `calc(${size} - 14px)`,
color: {color},
},
})}
</Box>
</Box>
)
}

View file

@ -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<Tag[]>([])
const [loading, setLoading] = useState(false)
// const [loading, setLoading] = useState(false)
const deviceAPI = useDeviceAPI();
const { error } = useSnackbar();
const [deviceTags, setDeviceTags] = useState<string[]>(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

View file

@ -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>(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 (
<Grid2 container spacing={1} marginBlock={1} >
<Grid2 size={{xs: 6, sm: 3}}>
<Card sx={{ padding: 1 }}>
<Grid2 container spacing={1} justifyContent={"center"} alignItems={"center"} >
<Grid2>
<CircleGraphIcon
sx={{ marginRight: 1, marginLeft: 1 }}
progress={100}
color={"info"}
icon={<AutoAwesomeMosaic />}
/>
</Grid2>
<Grid2 marginRight={1}>
<Typography variant="h6" >
{stats.total} Devices
</Typography>
<Typography variant="subtitle2" color="textSecondary">
Total
</Typography>
</Grid2>
</Grid2>
</Card>
</Grid2>
<Grid2 size={{xs: 6, sm: 3}}>
<Card sx={{ padding: 1 }}>
<Grid2 container spacing={1} justifyContent={"center"} alignItems={"center"} >
<Grid2>
<CircleGraphIcon
sx={{ marginRight: 1, marginLeft: 1 }}
progress={getPercent(stats.active)}
color={"success"}
icon={<Check />}
/>
</Grid2>
<Grid2 marginRight={1}>
<Typography variant="h6" >
{stats.active} Devices
</Typography>
<Typography variant="subtitle2" color="textSecondary">
Active
</Typography>
</Grid2>
</Grid2>
</Card>
</Grid2>
<Grid2 size={{xs: 6, sm: 3}}>
<Card sx={{ padding: 1 }}>
<Grid2 container spacing={1} justifyContent={"center"} alignItems={"center"} >
<Grid2>
<CircleGraphIcon
sx={{ marginRight: 1, marginLeft: 1 }}
progress={getPercent(stats.warning)}
color="warning"
icon={<Battery20 />}
/>
</Grid2>
<Grid2 marginRight={1}>
<Typography variant="h6" >
{stats.warning} Devices
</Typography>
<Typography variant="subtitle2" color="textSecondary">
Low Power
</Typography>
</Grid2>
</Grid2>
</Card>
</Grid2>
<Grid2 size={{xs: 6, sm: 3}}>
<Card sx={{ padding: 1 }}>
<Grid2 container spacing={1} justifyContent={"center"} alignItems={"center"} >
<Grid2>
<CircleGraphIcon
sx={{ marginRight: 1, marginLeft: 1 }}
progress={100}
color={green[50]}
icon={<PermScanWifi />}
/>
</Grid2>
<Grid2 marginRight={1}>
<Typography variant="h6" >
{stats.offline} Devices
</Typography>
<Typography variant="subtitle2" color="textSecondary">
Offline
</Typography>
</Grid2>
</Grid2>
</Card>
</Grid2>
</Grid2>
)
}

View file

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

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 { 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<Device[]>([])
const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false);
const [stats, setStats] = useState<pond.DeviceStatistics>(pond.DeviceStatistics.create())
const [loadingStats, setLoadingStats] = useState(false)
const [groupsLoading, setGroupsLoading] = useState(false)
const [groups, setGroups] = useState<Group[]>([]);
@ -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<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 /> */}
<Box
// onMouseEnter={() => 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() {
<Tab wrapped value={"never"} label={<CreateGroupIcon className={classes.green} />} onClick={() => openGroupSettings(undefined, "add")} />
</Tabs>
</Box>
<DevicesSummary />
<ResponsiveTable<Device>
title={<SmartBreadcrumb prependPaths={getGroup() && ["groups", getGroup().id().toString()]} groupName={getGroup() && getGroup().name()} paddingBottom={1}/>}
subtitle={subtitle()}

View file

@ -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 {

View file

@ -100,6 +100,10 @@ export interface IDeviceAPIContext {
keys?: string[],
types?: string[]
) => Promise<any>;
statistics: (
keys?: string[],
types?: string[]
) => Promise<any>;
tag: (id: number, tag: string) => Promise<any>;
untag: (id: number, tag: string) => Promise<any>;
getDatacap: (id: number) => Promise<any>;
@ -177,6 +181,22 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
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<pond.GetDeviceStatisticsResponse>(url);
};
const getDevicePageData = (
id: number | string,
keys?: string[],
@ -615,6 +635,7 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
update,
remove,
get: getDevice,
statistics,
getPageData: getDevicePageData,
getMulti,
getGeoJson: getDeviceGeoJSON,