import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon } from "@mui/icons-material"; import { Box, Chip, Grid2, IconButton, Skeleton, Tab, Tabs, Theme, Tooltip, Typography, useTheme } from "@mui/material"; import { blue, green, orange } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ProvisionDevice from "device/ProvisionDevice"; import { pond } from "protobuf-ts/pond"; import { useDeviceAPI, useGlobalState, useGroupAPI } from "providers"; import { useEffect, useState } from "react"; import PageContainer from "./PageContainer"; import { useMobile } from "hooks"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { getDeviceStateHelper } from "pbHelpers/DeviceState"; import { Device, Group, Tag } 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"; import BindaptIcon from "products/Bindapt/BindaptIcon"; import { describePower } from "pbHelpers/Power"; import { Tag as TagUI } from "common/Tag"; import moment from "moment"; import StatusPlenum from "common/StatusPlenum"; 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", width: theme.spacing(10) // justifyContent: "center", }, descriptionCellContainer: { margin: theme.spacing(1), marginLeft: theme.spacing(2), display: "flex", width: theme.spacing(26) // 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 Devices() { const theme = useTheme(); const isMobile = useMobile(); const classes = useStyles(); const navigate = useNavigate(); const location = useLocation(); const deviceAPI = useDeviceAPI(); const groupAPI = useGroupAPI(); const [devicesLoading, setDevicesLoading] = useState(false) const [limit, setLimit] = useState(10); const [page, setPage] = useState(0); const [order, ] = useState<"asc" | "desc">("asc"); const [orderBy, ] = useState("name"); const [search, setSearch] = useState(""); const [total, setTotal] = useState(0); const [devices, setDevices] = useState([]) const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(false); const [hasPlenums, setHasPlenums] = useState(false) const [groupsLoading, setGroupsLoading] = useState(false) const [groups, setGroups] = useState([]); const [groupLimit, ] = useState(10); const [groupPage, ] = useState(0); const [orderGroup, ] = useState<"asc" | "desc">("asc"); const [orderGroupBy, ] = useState("name"); const [searchGroup, ] = useState(""); // const [totalGroups, setTotalGroups] = useState(0); const [groupPermissions, setGroupPermissions] = useState([]) const groupID = useParams<{ groupID: string }>()?.groupID ?? "all"; const [tab, setTab] = useState(groupID==="all" ? groupID : groupID); const [selectedGroup, setSelectedGroup] = useState(undefined); const [groupSettingsMode, setGroupSettingsMode] = useState< "add" | "update" | "remove" | undefined >(undefined); const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState(false); const [fieldContains, setFieldContains] = useState>(new Map()) const [{ as }] = useGlobalState() const updateGroups = (newGroups: Group[]) => { setGroups(newGroups); }; const getGroup = () => { let group = groups[parseInt(tab)] return group } useEffect(() => { setGroupPermissions([]) if (tab === "all") return groupAPI.getGroupPermissions(getGroup().id()).then(resp => { console.log(resp) setGroupPermissions(resp.data.permissions) }) }, [tab]) const openProvisionDialog = () => { setIsProvisionDialogOpen(true); }; const closeProvisionDialog = () => { setIsProvisionDialogOpen(false); }; const openGroupSettings = ( group: Group | undefined, mode: "add" | "update" | "remove" | undefined ) => { setGroupSettingsIsOpen(true); setGroupSettingsMode(mode); setSelectedGroup(group); }; const closeGroupSettings = (_event: any) => { setGroupSettingsIsOpen(false); setGroupSettingsMode(undefined); setSelectedGroup(undefined); }; const getKeys = () => { if (tab !== "all") { let keys = getContextKeys() //keys.splice(keys.length - 1, 0, getGroup().id().toString()); keys.push(getGroup().id().toString()); return keys } else { return getContextKeys() } } const getTypes = () => { if (tab !== "all") { let types = getContextTypes() //types.splice(types.length - 1, 0, "group"); types.push("group"); return types } else { return getContextTypes() } } const loadGroups = () => { if (groupsLoading) return if (groups.length > 0) return setGroupsLoading(true) groupAPI.listGroups( groupLimit, groupPage*groupLimit, orderGroup, orderGroupBy, searchGroup, undefined ).then(resp => { let newGroups: Group[] = [] resp.data.groups?.forEach((group: pond.Group) => { newGroups.push(Group.create(group)) }) updateGroups(newGroups) }).finally(() => { setGroupsLoading(false) }) } const loadDevices = () => { setDevicesLoading(true) deviceAPI.list( limit, page*limit, order, orderBy, undefined, undefined, undefined, undefined, undefined, undefined, undefined, getKeys(), getTypes(), fieldContains, search, as ).then(resp => { let newDevices: Device[] = [] resp.data.devices.forEach(device => { if (device.status?.plenum?.temperature) { setHasPlenums(true) } newDevices.push(Device.create(device)) }) setDevices(newDevices) setTotal(resp.data.total) }).finally(() => { setDevicesLoading(false) }) } useEffect(() => { loadDevices() }, [limit, page, order, orderBy, fieldContains, search, tab, as]) useEffect(() => { loadGroups() }, [groupLimit, groupPage, orderGroup, orderGroupBy, searchGroup, as]) const handleChange = (event: any) => { setLimit(event.target.value); }; const prependToUrl = (prependage: number | string) => { const basePath = location.pathname.replace(/\/$/, ""); return(`/${prependage}/${basePath}`.replace(/\/{2,}/g, "/").replace(/(\/[^/]+)\/\1+/g, "$1")); }; const toDevice = (device: Device) => { let url = prependToUrl(getGroup() ? "groups/" + getGroup().id() : "") url = url + "/" + device.id() navigate(url, { replace: true, state: {device: device} }) }; const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => { if (newValue !== "never") setTab(newValue); if (newValue === "never") openGroupSettings(undefined, "add"); }; const columns = (): Column[] => { let columns = [ { title: "State", render: (device: Device) => { const status = device.status ?? pond.DeviceStatus.create() const deviceStateHelper = getDeviceStateHelper(status.state); return ( {deviceStateHelper.icon} ) } }, { title: "ID", render: (device: Device) => {device.settings?.deviceId} }, { title: "Device", render: (device: Device) => { return ( ) } }, { title: "Description", render: (device: Device) => { const description = device.settings?.description ?? "" return ( {description.length > 32 ? description.substring(0, 32) + "..." : description} ) } }, { title: "Tags", render: (device: Device) => { return ( {device.status.tags.map((tag: pond.TagSettings) => ( ))} ) } }, ] if (hasPlenums) { columns.push({ title: "Plenum", render: (device: Device) => { if (device.status.plenum?.temperature) { return ( ) } else { return ( <> ) } } }) } return columns } const provisionButton= () => { return ( ) } const groupButton = () => { return ( openGroupSettings(undefined, "add")} aria-label="Create Group"> ) } const subtitle = () => { return ( <> {provisionButton()} {groupButton()} ) } const mobile = (row: Device) => { return ( {/* {GetDeviceProductIcon(row)} */} {/* {icon} */} {row.name()} Last Active: {moment(row.status.lastActive).fromNow()} {describePower(row.status.power).icon} ) } const gutter = (row: Device) => { return ( {row.settings.description} {row.status.plenum && {row.status.plenum.temperature}°C {", "} {row.status.plenum.humidity}% } ) } return( 0 ? tab : "all"} onChange={handleTabChange} variant="scrollable" scrollButtons="auto" sx={{ borderRadius: 1 }} > {groupsLoading ? }/> : groups.map((group, index) => { if (group.id()) return ( {group.name()}} key={"group-tab-"+index}/> ) })} } onClick={() => openGroupSettings(undefined, "add")} /> title={} subtitle={subtitle} rows={devices} renderGutter={ isMobile ? gutter : undefined } columns={columns} renderMobile={mobile} total={total} pageSize={limit} page={page} setPage={setPage} handleRowsPerPageChange={handleChange} onRowClick={toDevice} setSearchText={setSearch} isLoading={devicesLoading} actions={getGroup() && } /> ) }