836 lines
No EOL
25 KiB
TypeScript
836 lines
No EOL
25 KiB
TypeScript
import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon, Tune } from "@mui/icons-material";
|
|
import { Box, Checkbox, Chip, Grid2, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, 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 { useCallback, 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";
|
|
import StatusSen5x from "common/StatusSen5x";
|
|
import StatusDust from "common/StatusDust";
|
|
import StatusGas from "common/StatusGas";
|
|
import { cloneDeep } from "lodash";
|
|
import LoadingScreen from "app/LoadingScreen";
|
|
|
|
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",
|
|
},
|
|
tagsCellContainer: {
|
|
margin: theme.spacing(1),
|
|
marginLeft: theme.spacing(2),
|
|
display: "flex",
|
|
width: theme.spacing(18)
|
|
// 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, setOrder] = useState<"asc" | "desc">(() => {
|
|
// Load from sessionStorage on initial render
|
|
const savedOrder = sessionStorage.getItem('order');
|
|
return (savedOrder === "asc" || savedOrder === "desc") ? savedOrder : "asc";
|
|
});
|
|
|
|
// Save to sessionStorage whenever order changes
|
|
useEffect(() => {
|
|
sessionStorage.setItem('order', order);
|
|
}, [order]);
|
|
|
|
const [orderBy, setOrderBy] = useState(() => {
|
|
// Load from sessionStorage on initial render
|
|
const savedOrder = sessionStorage.getItem('orderBy');
|
|
return (savedOrder && savedOrder?.length > 0) ? savedOrder : "name";
|
|
});
|
|
|
|
// Save to sessionStorage whenever order changes
|
|
useEffect(() => {
|
|
sessionStorage.setItem('orderBy', orderBy);
|
|
}, [orderBy]);
|
|
|
|
// const [orderBy, setOrderBy] = useState("name");
|
|
const [search, setSearch] = useState("");
|
|
const [total, setTotal] = useState(0);
|
|
const [devices, setDevices] = useState<Device[]>([])
|
|
const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false);
|
|
const [hasPlenums, setHasPlenums] = useState(false)
|
|
const [hasSen5x, setHasSen5x] = useState(false)
|
|
const [hasCo, setHasCo] = useState(false)
|
|
const [hasCo2, setHasCo2] = useState(false)
|
|
const [hasNo2, setHasNo2] = useState(false)
|
|
const [hasO2, setHasO2] = useState(false)
|
|
|
|
const [groupsLoading, setGroupsLoading] = useState(false)
|
|
const [groups, setGroups] = useState<Group[]>([]);
|
|
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<pond.Permission[]>([])
|
|
const [groupsLoaded, setGroupsLoaded] = useState(false)
|
|
|
|
const groupID = useParams<{ groupID: string }>()?.groupID ?? "all";
|
|
// const [tab, setTab] = useState(groupID==="all" ? groupID : groupID);
|
|
const [tab, setTab] = useState(() => {
|
|
console.log("groupID: "+groupID)
|
|
if (groupID!=="all") return groupID
|
|
const stored = sessionStorage.getItem(location.pathname+"-groups-tab");
|
|
console.log("stored: "+stored)
|
|
return stored !== null ? stored : "all";
|
|
});
|
|
|
|
useEffect(() => {
|
|
console.log(tab)
|
|
sessionStorage.setItem(location.pathname+"-groups-tab", tab);
|
|
}, [tab]);
|
|
|
|
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(undefined);
|
|
const [groupSettingsMode, setGroupSettingsMode] = useState<
|
|
"add" | "update" | "remove" | undefined
|
|
>(undefined);
|
|
const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState<boolean>(false);
|
|
|
|
const [fieldContains, setFieldContains] = useState<Map<string, string>>(new Map<string, string>())
|
|
|
|
const [separateDust, setSeparateDust] = useState(() => {
|
|
const stored = localStorage.getItem('separateDust');
|
|
return stored !== null ? stored === 'true' : false;
|
|
});
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('separateDust', separateDust.toString());
|
|
}, [separateDust]);
|
|
|
|
const [groupGas, setGroupGas] = useState(() => {
|
|
const stored = localStorage.getItem('groupGas');
|
|
return stored !== null ? stored === 'true' : false;
|
|
});
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('groupGas', groupGas.toString());
|
|
}, [groupGas]);
|
|
|
|
const [preferencesAnchor, setPreferencesAnchor] = useState<any>(null)
|
|
|
|
const [{ as }] = useGlobalState()
|
|
|
|
const updateGroups = (newGroups: Group[]) => {
|
|
setGroups(newGroups);
|
|
};
|
|
|
|
const getGroup = () => {
|
|
return groups[parseInt(tab)]
|
|
}
|
|
|
|
useEffect(() => {
|
|
setGroupPermissions([])
|
|
if (tab === "all" || !getGroup()) return
|
|
groupAPI.getGroupPermissions(getGroup().id()).then(resp => {
|
|
setGroupPermissions(resp.data.permissions)
|
|
})
|
|
}, [tab])
|
|
|
|
const openProvisionDialog = () => {
|
|
setIsProvisionDialogOpen(true);
|
|
};
|
|
|
|
const closeProvisionDialog = () => {
|
|
setIsProvisionDialogOpen(false);
|
|
};
|
|
|
|
const removeGroupCallback = useCallback(() => {
|
|
let newGroups = [...groups];
|
|
newGroups.splice(parseInt(tab), 1);
|
|
setGroups(newGroups);
|
|
setTab("all");
|
|
setGroupSettingsIsOpen(false);
|
|
}, [groups, tab]); // Include dependencies
|
|
|
|
const addGroupCallback = useCallback((groupSettings: pond.GroupSettings) => {
|
|
let newGroups = [...groups];
|
|
let group = pond.Group.create({
|
|
settings: groupSettings
|
|
})
|
|
newGroups.push(Group.create(group))
|
|
setGroups(newGroups);
|
|
// setTab(groups.length.toString())
|
|
}, [groups, tab]); // Include dependencies
|
|
|
|
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" && getGroup()) {
|
|
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,
|
|
getKeys(),
|
|
getTypes(),
|
|
).then(resp => {
|
|
let newGroups: Group[] = []
|
|
resp.data.groups?.forEach((group: pond.Group) => {
|
|
newGroups.push(Group.create(group))
|
|
})
|
|
updateGroups(newGroups)
|
|
}).finally(() => {
|
|
setGroupsLoading(false)
|
|
setGroupsLoaded(true)
|
|
})
|
|
}
|
|
|
|
const loadDevices = () => {
|
|
setDevicesLoading(true)
|
|
if (!groupsLoaded) return
|
|
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)
|
|
if (device.status?.sen5x?.temperature) setHasSen5x(true)
|
|
if (device.status?.o2) setHasO2(true)
|
|
if (device.status?.co) setHasCo(true)
|
|
if (device.status?.co2) setHasCo2(true)
|
|
if (device.status?.no2) setHasNo2(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, groupsLoaded])
|
|
|
|
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"));
|
|
// };
|
|
|
|
function insertGroupContext(groupID: string, deviceID: string): string {
|
|
const path = location.pathname
|
|
const segments = path.split('/').filter(Boolean);
|
|
const deviceIndex = segments.findIndex(segment => segment === 'devices');
|
|
|
|
if (deviceIndex === -1) {
|
|
throw new Error('Path does not contain "devices"');
|
|
}
|
|
|
|
// Build the new segments
|
|
const newSegments = [
|
|
...segments.slice(0, deviceIndex),
|
|
'groups',
|
|
groupID,
|
|
...segments.slice(deviceIndex),
|
|
deviceID
|
|
];
|
|
|
|
return '/' + newSegments.join('/');
|
|
}
|
|
|
|
// useEffect(() => {
|
|
// console.log(devices)
|
|
// }, [devices])
|
|
|
|
const toDevice = (device: Device) => {
|
|
let url = getGroup() ? insertGroupContext(getGroup().id().toString(), device.id().toString()) : ""
|
|
if (url.length < 1) url = device.id().toString()
|
|
navigate(url, { state: {device: device} })
|
|
};
|
|
|
|
const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => {
|
|
if (newValue !== "never") setTab(newValue);
|
|
if (newValue === "never") openGroupSettings(undefined, "add");
|
|
};
|
|
|
|
const columns = (): Column<Device>[] => {
|
|
let columns: Column<Device>[] = [
|
|
{
|
|
title: "State",
|
|
// cellStyle: {width: 100000},
|
|
// sortKey: "state",
|
|
render: (device: Device) => {
|
|
const status = device.status ?? pond.DeviceStatus.create()
|
|
const deviceStateHelper = getDeviceStateHelper(status.state);
|
|
return (
|
|
<Box className={classes.cellContainer} style={{ width: theme.spacing(0.5)}}>
|
|
{/* <Box > */}
|
|
<Tooltip title={deviceStateHelper.description}>{deviceStateHelper.icon}</Tooltip>
|
|
</Box>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
title: "ID",
|
|
sortKey: "deviceId",
|
|
render: (device: Device) => <span className={classes.cellContainer}>
|
|
{device.settings?.deviceId}
|
|
</span>
|
|
},
|
|
{
|
|
title: "Device",
|
|
cellStyle: { width: 160 },
|
|
sortKey: "name",
|
|
render: (device: Device) => {
|
|
return (
|
|
<Box className={classes.cellContainer}>
|
|
{/* // <Box > */}
|
|
<Chip
|
|
variant="outlined"
|
|
label={device.settings?.name ? device.settings.name : "Device " + device.settings?.deviceId}
|
|
sx={{ maxWidth: 160}}
|
|
/>
|
|
</Box>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
title: "Description",
|
|
cellStyle: { width: 400 },
|
|
render: (device: Device) => {
|
|
const description = device.settings?.description ?? ""
|
|
let size = (hasCo2 && !groupGas) ? 26 : 36
|
|
return (
|
|
<Box className={classes.descriptionCellContainer} >
|
|
<Tooltip title={description}>
|
|
<span>
|
|
{description.length > size
|
|
? description.substring(0, size) + "..."
|
|
: description}
|
|
</span>
|
|
</Tooltip>
|
|
</Box>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
title: "Tags",
|
|
sortKey: "tags",
|
|
render: (device: Device) => {
|
|
return (
|
|
<Grid2 container spacing={1} className={classes.tagsCellContainer}>
|
|
{device.status.tags.map((tag: pond.TagSettings) => (
|
|
<Grid2 key={"device-"+device.id()+"-tag-"+tag.key}>
|
|
<TagUI tag={Tag.any({ settings: tag })} />
|
|
</Grid2>
|
|
))}
|
|
</Grid2>
|
|
)
|
|
}
|
|
},
|
|
]
|
|
if (hasPlenums) {
|
|
columns.push({
|
|
title: "Plenum",
|
|
// sortKey: "hi",
|
|
// disableSort: true,
|
|
render: (device: Device) => {
|
|
if (device.status.plenum?.temperature) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusPlenum device={device} />
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if (hasSen5x) {
|
|
columns.push({
|
|
title: "Sen5X",
|
|
// sortKey: "hi",
|
|
// disableSort: true,
|
|
render: (device: Device) => {
|
|
if (device.status.sen5x?.temperature) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusSen5x device={device} noDust={separateDust} />
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
if (separateDust) columns.push({
|
|
title: "Dust",
|
|
render: (device: Device) => {
|
|
if (device.status.sen5x?.temperature) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusDust device={device} />
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if (hasCo) {
|
|
columns.push({
|
|
title: groupGas ? "Gas" : "CO",
|
|
render: (device: Device) => {
|
|
if (device.status.co) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusGas device={device} gasType={groupGas ? "all" : "co"}/>
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if (hasCo2 && !groupGas) {
|
|
columns.push({
|
|
title: "CO2",
|
|
render: (device: Device) => {
|
|
if (device.status.co2) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusGas device={device} gasType={"co2"}/>
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if (hasNo2 && !groupGas) {
|
|
columns.push({
|
|
title: "NO2",
|
|
render: (device: Device) => {
|
|
if (device.status.no2) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusGas device={device} gasType={"no2"}/>
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if (hasO2 && !groupGas) {
|
|
columns.push({
|
|
title: "O2",
|
|
render: (device: Device) => {
|
|
if (device.status.o2) {
|
|
return (
|
|
<Box sx={{ marginLeft: 1 }}>
|
|
<StatusGas device={device} gasType={"o2"}/>
|
|
</Box>
|
|
)
|
|
} else {
|
|
return (
|
|
<></>
|
|
)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return columns
|
|
}
|
|
|
|
const provisionButton= () => {
|
|
return (
|
|
<Tooltip title="Provision Device">
|
|
<IconButton onClick={openProvisionDialog} aria-label="Provision Device">
|
|
<ProvisionIcon className={classes.provisionIcon} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
const groupButton = () => {
|
|
return (
|
|
<Tooltip title="Create Group">
|
|
<IconButton
|
|
onClick={() => openGroupSettings(undefined, "add")}
|
|
aria-label="Create Group">
|
|
<CreateGroupIcon className={classes.green} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
const subtitle = () => {
|
|
return (
|
|
<>
|
|
{provisionButton()}
|
|
{groupButton()}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const mobile = (row: Device) => {
|
|
return (
|
|
<Box sx={{ margin: 2 }}>
|
|
<Grid2 spacing={1} container direction={"row"} justifyContent="space-between" alignItems={"center"}>
|
|
<Grid2 >
|
|
{/* {GetDeviceProductIcon(row)} */}
|
|
{/* {icon} */}
|
|
<BindaptIcon />
|
|
</Grid2>
|
|
<Grid2 size={{ xs: 9}}>
|
|
<Grid2 container direction="column">
|
|
<Grid2>
|
|
{row.name()}
|
|
</Grid2>
|
|
<Grid2>
|
|
Last Active: {moment(row.status.lastActive).fromNow()}
|
|
</Grid2>
|
|
</Grid2>
|
|
</Grid2>
|
|
<Grid2 >
|
|
{describePower(row.status.power).icon}
|
|
</Grid2>
|
|
</Grid2>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const gutter = (row: Device) => {
|
|
return (
|
|
<Box sx={{ margin: 1 }}>
|
|
<Grid2 container spacing={1} direction="column">
|
|
<Grid2>
|
|
{row.settings.description}
|
|
</Grid2>
|
|
{row.status.plenum &&
|
|
<Grid2 container direction="row" spacing={0}>
|
|
<Grid2>
|
|
<Typography color={orange[400]}>
|
|
{row.status.plenum.temperature}°C
|
|
</Typography>
|
|
</Grid2>
|
|
<Grid2 >
|
|
<Typography style={{ marginRight: 6 }}>
|
|
{", "}
|
|
</Typography>
|
|
</Grid2>
|
|
<Grid2>
|
|
<Typography color={blue[300]}>
|
|
{row.status.plenum.humidity}%
|
|
</Typography>
|
|
</Grid2>
|
|
</Grid2>
|
|
}
|
|
</Grid2>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const preferencesButton = () => {
|
|
return (
|
|
<IconButton onClick={(event) => setPreferencesAnchor(event.currentTarget)}>
|
|
<Tune />
|
|
</IconButton>
|
|
)
|
|
}
|
|
|
|
const preferencesMenu = () => {
|
|
return (
|
|
<Menu
|
|
id="userMenu"
|
|
anchorEl={preferencesAnchor}
|
|
open={preferencesAnchor !== null}
|
|
onClose={() => setPreferencesAnchor(null)}
|
|
disableAutoFocusItem>
|
|
<MenuItem
|
|
onClick={() => setSeparateDust(!separateDust)}
|
|
sx={{ marginLeft: -0.75 }}
|
|
dense
|
|
>
|
|
<ListItemIcon>
|
|
<Checkbox checked={!separateDust} />
|
|
</ListItemIcon>
|
|
<ListItemText primary={"Group Dust"} />
|
|
</MenuItem>
|
|
<MenuItem
|
|
onClick={() => setGroupGas(!groupGas)}
|
|
sx={{ marginLeft: -0.75 }}
|
|
dense
|
|
>
|
|
<ListItemIcon>
|
|
<Checkbox checked={groupGas} />
|
|
</ListItemIcon>
|
|
<ListItemText primary={"Group Gas"} />
|
|
</MenuItem>
|
|
</Menu>
|
|
)
|
|
}
|
|
|
|
useEffect(() => {
|
|
console.log('Current tab value:', tab, typeof tab);
|
|
}, [tab]);
|
|
|
|
if (!groupsLoaded) return (
|
|
<LoadingScreen message="Loading groups" />
|
|
)
|
|
|
|
return(
|
|
<PageContainer padding={isMobile ? 0 : 2}>
|
|
<Box
|
|
sx={{
|
|
borderRadius: 1,
|
|
border: "1px solid rgba(150, 150, 150, 0.2)",
|
|
marginTop: 0,
|
|
marginBottom: 1
|
|
}}
|
|
>
|
|
<Tabs
|
|
value={groups.length > 0 ? tab : "all"}
|
|
onChange={handleTabChange}
|
|
variant="scrollable"
|
|
scrollButtons="auto"
|
|
sx={{ borderRadius: 1 }}
|
|
>
|
|
<Tab wrapped value={"all"} label="All" />
|
|
{groupsLoading ?
|
|
<Tab value={"loading"} label={<Skeleton variant="rectangular" height={24} width={128} animation="pulse" />}/>
|
|
: groups.map((group, index) => {
|
|
if (group.id()) return (
|
|
<Tab className={classes.tab} wrapped value={index.toString()} label={<Typography variant="inherit" noWrap>{group.name()}</Typography>} key={"group-tab-"+index}/>
|
|
)
|
|
})}
|
|
<Tab wrapped value={"never"} label={<CreateGroupIcon className={classes.green} />} onClick={() => openGroupSettings(undefined, "add")} />
|
|
</Tabs>
|
|
</Box>
|
|
<DevicesSummary setFieldContains={setFieldContains} group={getGroup()} />
|
|
<ResponsiveTable<Device>
|
|
title={<SmartBreadcrumb prependPaths={getGroup() && ["groups", getGroup().id().toString()]} groupName={getGroup() && getGroup().name()} paddingBottom={1}/>}
|
|
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() && <GroupActions removeCallback={removeGroupCallback} group={getGroup()} permissions={groupPermissions} devices={devices} refreshCallback={loadDevices}/>}
|
|
order={order}
|
|
setOrder={setOrder}
|
|
orderBy={orderBy}
|
|
setOrderBy={setOrderBy}
|
|
endTitleElement={preferencesButton}
|
|
loadMore={()=>{
|
|
let currentRows = cloneDeep(devices)
|
|
deviceAPI.list(
|
|
limit,
|
|
currentRows.length,
|
|
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)
|
|
if (device.status?.sen5x?.temperature) setHasSen5x(true)
|
|
if (device.status?.o2) setHasO2(true)
|
|
if (device.status?.co) setHasCo(true)
|
|
if (device.status?.co2) setHasCo2(true)
|
|
if (device.status?.no2) setHasNo2(true)
|
|
newDevices.push(Device.create(device))
|
|
})
|
|
setDevices(currentRows.concat(newDevices))
|
|
}).catch(_err => {
|
|
|
|
})
|
|
}}
|
|
/>
|
|
<ProvisionDevice
|
|
isOpen={isProvisionDialogOpen}
|
|
refreshCallback={loadDevices}
|
|
closeDialogCallback={closeProvisionDialog}
|
|
/>
|
|
{/* This GroupSettings is used for adding */}
|
|
<GroupSettings
|
|
initialGroup={selectedGroup}
|
|
mode={groupSettingsMode}
|
|
isDialogOpen={groupSettingsIsOpen}
|
|
closeDialogCallback={closeGroupSettings}
|
|
refreshCallback={() => {}}
|
|
canEdit={true}
|
|
groupDevices={devices}
|
|
removeGroupCallback={removeGroupCallback}
|
|
addGroupCallback={addGroupCallback}
|
|
/>
|
|
{preferencesMenu()}
|
|
</PageContainer>
|
|
)
|
|
} |