frontend/src/common/DeviceLinkDrawer.tsx

331 lines
9.9 KiB
TypeScript

import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Checkbox,
CircularProgress,
Drawer,
List,
ListItem,
ListItemText,
Tab,
Tabs,
TextField
} from "@mui/material";
import { ExpandMore } from "@mui/icons-material";
import SearchBar from "common/SearchBar";
import { useDeviceAPI, useMobile } from "hooks";
import { Component, Device } from "models";
import { pond } from "protobuf-ts/pond";
import React, { useEffect, useState } from "react";
import { makeStyles } from "@mui/styles";
import { useGlobalState } from "providers";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
const useStyles = makeStyles(() => ({
drawerPaperDesktop: {
height: "100%",
width: "40%"
},
drawerPaperMobile: {
height: "60%",
width: "100%"
}
}));
interface Props {
open: boolean;
deviceTags?: string[];
close: (reload?: boolean) => void;
linkedDevices: Map<string, pond.ComprehensiveDevice>;
updateLinkedDevices: (dev: pond.ComprehensiveDevice, linked: boolean) => void;
linkedComponents?: Component[];
updateLinkedComponents?: (
deviceID: string | number,
componen: Component,
linked: boolean
) => void;
//all of these need to be sent in for the device preference selector to work
devicePrefMap?: Map<number, number>; //map using the device id as the key and its pref enums value as the value
prefOptions?: JSX.Element[];
devicePrefChanged?: (device: Device, newPref: number) => void;
}
export default function DeviceLinkDrawer(props: Props) {
const {
open,
close,
linkedDevices,
linkedComponents,
updateLinkedDevices,
updateLinkedComponents,
deviceTags,
devicePrefMap,
prefOptions,
devicePrefChanged
} = props;
const [value, setValue] = useState(0);
const [{as}] = useGlobalState();
const deviceAPI = useDeviceAPI();
const classes = useStyles();
const isMobile = useMobile();
const [deviceList, setDeviceList] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [searchVal, setSearchVal] = useState("");
const [searchedDevices, setSearchedDevices] = useState<Map<string, pond.ComprehensiveDevice>>(
new Map<string, pond.ComprehensiveDevice>()
);
const [accordionController, setAccordionController] = useState<boolean[]>([]);
const [loadingDevices, setLoadingDevices] = useState(false);
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
setValue(newValue);
};
useEffect(() => {
if (loadingDevices) return;
setLoadingDevices(true);
deviceAPI
.list(
500,
0,
"asc",
"key",
deviceTags?.toString(),
undefined,
undefined,
undefined,
undefined,
true,
undefined,
undefined,
undefined,
undefined,
undefined,
as
)
.then(resp => {
let devMap = new Map<string, pond.ComprehensiveDevice>();
resp.data.comprehensiveDevices.forEach(compDev => {
if (compDev.device?.settings?.deviceId) {
devMap.set(compDev.device?.settings?.deviceId.toString(), compDev);
}
});
setDeviceList(devMap);
setSearchedDevices(devMap);
})
.catch(err => {})
.finally(() => {
setLoadingDevices(false);
});
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [deviceAPI, as]);
useEffect(() => {
let searchedDevices: Map<string, pond.ComprehensiveDevice> = new Map();
if (searchVal !== "") {
deviceList.forEach(dev => {
if (dev.device?.settings?.name) {
if (
dev.device?.settings?.name.toLowerCase().includes(searchVal.toLowerCase()) ||
dev.device?.settings?.deviceId.toString().includes(searchVal)
) {
searchedDevices.set(dev.device.settings.deviceId.toString(), dev);
}
} else {
if (dev.device?.settings?.deviceId.toString().includes(searchVal)) {
searchedDevices.set(dev.device.settings.deviceId.toString(), dev);
}
}
});
setSearchedDevices(searchedDevices);
} else {
setSearchedDevices(deviceList);
}
}, [searchVal, deviceList]);
//sets the length of the array of booleans to control opening each of the accordions
useEffect(() => {
let accordionControls: boolean[] = [];
linkedDevices.forEach(() => {
accordionControls.push(false);
});
deviceList.forEach(dev => {
if (
dev.device?.settings?.deviceId &&
!linkedDevices.get(dev.device?.settings?.deviceId.toString())
) {
accordionControls.push(false);
}
});
setAccordionController(accordionControls);
}, [linkedDevices, deviceList]);
const closeDrawer = () => {
close();
};
const devicesTab = () => {
return (
<Box>
Select Devices to link
<List>
<SearchBar
value={searchVal}
onChange={val => {
setSearchVal(val);
}}
/>
{Array.from(searchedDevices.values()).map(dev => {
let defCheck = linkedDevices.has(dev.device?.settings?.deviceId.toString() ?? "");
return (
<ListItem key={dev.device?.settings?.deviceId}>
<ListItemText
primary={dev.device?.settings?.name ?? "Device " + dev.device?.settings?.deviceId}
/>
<Checkbox
onChange={(_, checked) => {
if (dev.device?.settings?.deviceId) {
updateLinkedDevices(dev, checked);
}
}}
edge="end"
defaultChecked={defCheck}
inputProps={{ "aria-labelledby": dev.device?.settings?.name }}
/>
</ListItem>
);
})}
</List>
</Box>
);
};
const linkedDevicesTab = () => {
let linkOptions: pond.ComprehensiveDevice[] = [];
Array.from(linkedDevices.keys()).forEach(key => {
let dev = deviceList.get(key);
let linkedDev = linkedDevices.get(key);
if (dev) {
linkOptions.push(dev);
} else if (linkedDev) {
linkOptions.push(linkedDev);
}
});
return (
<Box>
From the linked Devices select which components to use
{linkOptions.map((op, i) => {
let device = Device.any(op.device);
let pref = 0;
if (devicePrefMap) {
pref = devicePrefMap.get(device.id()) ?? 0;
}
return (
<Accordion
key={device.id()}
expanded={accordionController[i]}
onChange={(_, expanded) => {
let accordionControls = accordionController;
accordionControls[i] = expanded;
setAccordionController([...accordionControls]);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>{device.name()}</AccordionSummary>
<AccordionDetails>
<List>
{op.components.map(comp => {
let component = Component.any(comp);
return (
<ListItem key={component.key()}>
<ListItemText primary={component.name()} />
<Checkbox
onChange={(_, checked) => {
if (component.key() && device.id() && updateLinkedComponents) {
updateLinkedComponents(device.id(), component, checked);
}
}}
edge="end"
checked={
linkedComponents &&
linkedComponents.some(el => el.key() === component.key())
}
inputProps={{ "aria-labelledby": component.name() }}
/>
</ListItem>
);
})}
</List>
{devicePrefMap && prefOptions && devicePrefChanged && (
<TextField
title="Gate Device Type"
select
value={pref}
variant="outlined"
onChange={e => {
pref = parseInt(e.target.value);
devicePrefChanged(device, pref);
}}
fullWidth>
{prefOptions}
</TextField>
)}
</AccordionDetails>
</Accordion>
);
})}
</Box>
);
};
return (
<Drawer
open={open}
onClose={closeDrawer}
classes={{ paper: isMobile ? classes.drawerPaperMobile : classes.drawerPaperDesktop }}
anchor={isMobile ? "bottom" : "right"}>
{open && (
<React.Fragment>
<Tabs value={value} indicatorColor="primary" textColor="primary" onChange={handleChange}>
<Tab label="Devices" />
{linkedComponents && <Tab label="Linked Devices" />}
</Tabs>
<TabPanelMine value={value} index={0}>
{loadingDevices ? (
<Box style={{ textAlign: "center", paddingTop: 20 }}>
<CircularProgress />
</Box>
) : (
devicesTab()
)}
</TabPanelMine>
{linkedComponents && (
<TabPanelMine value={value} index={1}>
{linkedDevicesTab()}
</TabPanelMine>
)}
</React.Fragment>
)}
</Drawer>
);
}