merged gas status, pulled, updated staging proto

This commit is contained in:
Carter 2025-05-23 13:24:32 -06:00
commit a1a4851fd9
8 changed files with 593 additions and 67 deletions

View file

@ -98,6 +98,7 @@ interface Props<T> {
setOrderBy?: React.Dispatch<React.SetStateAction<string>>; setOrderBy?: React.Dispatch<React.SetStateAction<string>>;
order?: string; order?: string;
setOrder?: React.Dispatch<React.SetStateAction<"asc" | "desc">>; setOrder?: React.Dispatch<React.SetStateAction<"asc" | "desc">>;
endTitleElement?: string | JSX.Element | (() => string | JSX.Element);
} }
export default function ResponsiveTable<T extends Object>(props: Props<T>) { export default function ResponsiveTable<T extends Object>(props: Props<T>) {
@ -137,6 +138,7 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
const subtitle = typeof(props.subtitle) === "function" ? props.subtitle() : props.subtitle const subtitle = typeof(props.subtitle) === "function" ? props.subtitle() : props.subtitle
const title = typeof(props.title) === "function" ? props.title() : props.title const title = typeof(props.title) === "function" ? props.title() : props.title
// const order = props.order ? props.order : "asc" // const order = props.order ? props.order : "asc"
const endTitleElement = typeof(props.endTitleElement) === "function" ? props.endTitleElement() : props.endTitleElement
const isMobile = useMobile() const isMobile = useMobile()
@ -318,11 +320,13 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
if (isMobile || mobileView) return ( if (isMobile || mobileView) return (
<Box> <Box>
<Box className={classes.mobileTitleBox}> <Box className={classes.mobileTitleBox}>
{renderTitle()} {renderTitle()}
{renderSubtitle()} {renderSubtitle()}
{setSearchText && searchBar()} {setSearchText && searchBar()}
</Box> </Box>
{rows.map((row, index) => { {rows.map((row, index) => {
return ( return (
<Card className={classNames(classes.card)} key={"mobile-card-"+index} onClick={() => onRowClick&&onRowClick(row)}> <Card className={classNames(classes.card)} key={"mobile-card-"+index} onClick={() => onRowClick&&onRowClick(row)}>
@ -422,6 +426,9 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
<Grid2> <Grid2>
{actions && actions} {actions && actions}
</Grid2> </Grid2>
<Grid2>
{endTitleElement}
</Grid2>
<Grid2> <Grid2>
{setSearchText &&<Box className={classes.searchContainer}> {setSearchText &&<Box className={classes.searchContainer}>
{searchBar()} {searchBar()}

149
src/common/StatusDust.tsx Normal file
View file

@ -0,0 +1,149 @@
import { Box, Grid2, Theme, Tooltip, Typography } from "@mui/material";
import { green } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Device } from "models";
import PulseBox from "./PulseBox";
import { or } from "utils";
import { useEffect, useState } from "react";
interface Props {
device: Device
}
const useStyles = makeStyles((theme: Theme) => {
const lightMode = theme.palette?.mode === "light";
return ({
box: {
display: "inline-block",
borderRadius: "6px",
backgroundColor: lightMode ? "rgb(210, 210, 210)" : "rgb(50, 50, 50)",
padding: theme.spacing(0.75),
marginRight: "auto",
margin: theme.spacing(1),
width: theme.spacing(16),
height: theme.spacing(11),
},
carouselContainer: {
position: 'relative',
height: '40px',
overflow: 'hidden',
textAlign: "center",
},
carouselItem: {
opacity: 0,
transform: 'translateX(100%)',
transition: 'opacity 0.5s ease, transform 0.5s ease',
position: 'absolute',
width: '100%',
overflow: "hidden",
// height: '100%',
},
active: {
opacity: 1,
transform: 'translateX(0)',
},
inactive: {
transform: 'translateX(-100%)',
},
})
})
export default function StatusDust(props: Props) {
const { device } = props;
const classes = useStyles()
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []);
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setColorIndex(prevIndex => {
const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1;
setColor(colors[newIndex]); // Use the new index here
return newIndex;
});
}, 7500);
return () => clearInterval(interval);
}, []);
// const [currentIndex, setCurrentIndex] = useState(0);
// Auto-cycle through measurements every 3 seconds
// useEffect(() => {
// const interval = setInterval(() => {
// setCurrentIndex((prevIndex) => (prevIndex + 1) % 2);
// }, 3000); // Adjust timing as needed (3000ms = 3s)
// return () => clearInterval(interval); // Cleanup on unmount
// }, []);
const tooltip = () => {
if (messages.length === 0) return null
if (messages.length < 2) return (
messages[0]
)
return (
<Grid2 container direction={"column"}>
<Grid2 container direction="row">
<Typography fontWeight={"bold"} variant="caption">
Overlay Messages
</Typography>
</Grid2>
{messages.map((message, index) => {
return (
<Grid2 key={"tooltip-overly-message-" + message} container direction="row" alignItems={"center"}>
<Box
sx={{
width: 10, // Size of the square
height: 10,
backgroundColor: colors[index], // Passed color prop
borderRadius: 1, // Optional: slight rounding, remove for sharp square
border: "1px solid black",
}}
/>
<Typography variant="caption" sx={{ marginLeft: 1 }}>
{message}
</Typography>
</Grid2>
)
})}
</Grid2>
)
}
return (
<Tooltip title={tooltip()}>
<PulseBox color={color} className={classes.box} >
<Grid2 container direction="column" >
<Grid2 container direction="column"
className={`${classes.carouselItem} ${classes.active }`}
>
<Grid2>
<Typography variant="body2" style={{ color: green[300]}}>
{"<1um:"} {device.status.sen5x?.dust1ug.toFixed(1)}ug/m3
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: green[400]}}>
{"<2.5um:"} {device.status.sen5x?.dust2ug.toFixed(1)}ug/m3
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: green[500]}}>
{"<4um: "}{device.status.sen5x?.dust4ug.toFixed(1)}ug/m3
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: green[600]}}>
{"<10um: "}{device.status.sen5x?.dust10ug.toFixed(1)}ug/m3
</Typography>
</Grid2>
</Grid2>
</Grid2>
</PulseBox>
</Tooltip>
)
}

199
src/common/StatusGas.tsx Normal file
View file

@ -0,0 +1,199 @@
import { Box, Grid2, Theme, Tooltip, Typography } from "@mui/material";
import { blue, deepOrange, green, grey, orange, teal } from "@mui/material/colors";
import { makeStyles } from "@mui/styles";
import { Device } from "models";
import PulseBox from "./PulseBox";
import { or } from "utils";
import { useEffect, useState } from "react";
import { pond } from "protobuf-ts/pond";
interface Props {
device: Device
// noDust?: boolean;
gasType: "co2" | "co" | "no2" | "o2" | "all"
}
const useStyles = makeStyles((theme: Theme) => {
const lightMode = theme.palette?.mode === "light";
return ({
box: {
display: "inline-block",
borderRadius: "6px",
backgroundColor: lightMode ? "rgb(210, 210, 210)" : "rgb(50, 50, 50)",
padding: theme.spacing(0.75),
marginRight: "auto",
margin: theme.spacing(1),
width: theme.spacing(14),
height: theme.spacing(6),
},
boxTall: {
display: "inline-block",
borderRadius: "6px",
backgroundColor: lightMode ? "rgb(210, 210, 210)" : "rgb(50, 50, 50)",
padding: theme.spacing(0.75),
marginRight: "auto",
margin: theme.spacing(1),
width: theme.spacing(14),
height: theme.spacing(8),
},
carouselContainer: {
position: 'relative',
height: '40px',
overflow: 'hidden',
textAlign: "center",
},
carouselItem: {
opacity: 0,
transform: 'translateX(100%)',
transition: 'opacity 0.5s ease, transform 0.5s ease',
position: 'absolute',
width: '100%',
overflow: "hidden",
// height: '100%',
},
active: {
opacity: 1,
transform: 'translateX(0)',
},
inactive: {
transform: 'translateX(-100%)',
},
})
})
export default function StatusGas(props: Props) {
const { device, gasType } = props;
const classes = useStyles()
// console.log(noDust)
let colors: string[] = []
let messages: string[] = []
if (gasType === "all") {
colors = or(device.status.o2?.overlays.map(overlay => overlay.color), []);
colors.push(...or(device.status.co2?.overlays.map(overlay => overlay.color), []));
colors.push(...or(device.status.no2?.overlays.map(overlay => overlay.color), []));
colors.push(...or(device.status.co?.overlays.map(overlay => overlay.color), []));
messages = or(device.status.o2?.overlays.map(overlay => overlay.message), []);
messages.push(...or(device.status.co2?.overlays.map(overlay => overlay.message), []));
messages.push(...or(device.status.no2?.overlays.map(overlay => overlay.message), []));
messages.push(...or(device.status.co?.overlays.map(overlay => overlay.message), []));
} else {
colors = or(device.status[gasType]?.overlays.map(overlay => overlay.color), []);
messages = or(device.status[gasType]?.overlays.map(overlay => overlay.message), []);
}
const [color, setColor] = useState(colors.length > 0 ? colors[0] : "");
const [, setColorIndex] = useState(0)
const gasTypes: ("o2" | "no2" | "co2" | "co")[] = ["o2", "no2", "co2", "co"]
useEffect(() => {
const interval = setInterval(() => {
setColorIndex(prevIndex => {
const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1;
setColor(colors[newIndex]); // Use the new index here
return newIndex;
});
}, 7500);
return () => clearInterval(interval);
}, []);
const [currentIndex, setCurrentIndex] = useState(0);
// Auto-cycle through measurements every 5 seconds
useEffect(() => {
if (gasType !== "all") {
setCurrentIndex(0)
return
}
const interval = setInterval(() => {
if (gasType === "all") setCurrentIndex((prevIndex) => (prevIndex + 1) % 4);
else setCurrentIndex((prevIndex) => (prevIndex + 1) % 4);
}, 6000);
return () => clearInterval(interval); // Cleanup on unmount
}, [gasType]);
const tooltip = () => {
if (messages.length === 0) return null
if (messages.length < 2) return (
messages[0]
)
return (
<Grid2 container direction={"column"}>
<Grid2 container direction="row">
<Typography fontWeight={"bold"} variant="caption">
Overlay Messages
</Typography>
</Grid2>
{messages.map((message, index) => {
return (
<Grid2 key={"tooltip-overly-message-" + message} container direction="row" alignContent={"center"}>
<Grid2 size={{xs: 1}} alignContent={"center"}>
<Box
sx={{
width: 12, // Size of the square
height: 12,
backgroundColor: colors[index], // Passed color prop
borderRadius: 3, // Optional: slight rounding, remove for sharp square
border: "1px solid black",
marginBottom: 0.4
}}
/>
</Grid2>
<Grid2 size={{ xs: 11 }} alignContent={"center"}>
<Typography variant="caption">
{message}
</Typography>
</Grid2>
</Grid2>
)
})}
</Grid2>
)
}
const gasDisplay = () => {
const gas = gasType === "all" ? gasTypes[currentIndex] : gasType
// if (gasType !== "all") {
return (
<Tooltip title={tooltip()}>
<PulseBox color={color} className={gasType === "all" ? classes.boxTall : classes.box}>
<Grid2 container direction="column" >
{ gasType === "all" &&
<Typography variant="body2">
{gas.toUpperCase()}
</Typography>
}
<Grid2 container direction="column">
<Grid2>
{gas !== "o2" ?
<Typography variant="body2" style={{ color: teal[500]}}>
Gas: {device.status[gas]?.ppm.toFixed(1)}ppm
</Typography>
:
<Typography variant="body2" style={{ color: blue[500]}}>
Gas: {(device.status[gas]?.ppm!/10000).toFixed(1)}%
</Typography>
}
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: deepOrange[800]}}>
Volt: {device.status[gas]?.millivolts.toFixed(1)}mV
</Typography>
</Grid2>
</Grid2>
</Grid2>
</PulseBox>
</Tooltip>
)
// }
}
return (
<>
{gasDisplay()}
</>
)
}

View file

@ -5,8 +5,10 @@ import { Device } from "models";
import PulseBox from "./PulseBox"; import PulseBox from "./PulseBox";
import { or } from "utils"; import { or } from "utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
interface Props { interface Props {
device: Device device: Device
noDust?: boolean;
} }
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
@ -19,8 +21,8 @@ const useStyles = makeStyles((theme: Theme) => {
padding: theme.spacing(0.75), padding: theme.spacing(0.75),
marginRight: "auto", marginRight: "auto",
margin: theme.spacing(1), margin: theme.spacing(1),
width: "72px", width: theme.spacing(16),
height: "50px", height: theme.spacing(11),
}, },
carouselContainer: { carouselContainer: {
position: 'relative', position: 'relative',
@ -47,10 +49,10 @@ const useStyles = makeStyles((theme: Theme) => {
}) })
}) })
export default function StatusPlenum(props: Props) { export default function StatusSen5x(props: Props) {
const { device } = props; const { device, noDust } = props;
const classes = useStyles() const classes = useStyles()
// console.log(noDust)
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []); const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []); const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []);
@ -71,14 +73,18 @@ export default function StatusPlenum(props: Props) {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
// Auto-cycle through measurements every 3 seconds // Auto-cycle through measurements every 5 seconds
useEffect(() => { useEffect(() => {
if (noDust) {
setCurrentIndex(0)
return
}
const interval = setInterval(() => { const interval = setInterval(() => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); if (!noDust) setCurrentIndex((prevIndex) => (prevIndex + 1) % 2);
}, 3000); // Adjust timing as needed (3000ms = 3s) }, 5000);
return () => clearInterval(interval); // Cleanup on unmount return () => clearInterval(interval); // Cleanup on unmount
}, []); }, [noDust]);
const tooltip = () => { const tooltip = () => {
if (messages.length === 0) return null if (messages.length === 0) return null
@ -125,66 +131,52 @@ export default function StatusPlenum(props: Props) {
> >
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: orange[700]}}> <Typography variant="body2" style={{ color: orange[700]}}>
{device.status.sen5x?.temperature.toFixed(1)}°C Temp: {device.status.sen5x?.temperature.toFixed(1)}°C
</Typography> </Typography>
</Grid2> </Grid2>
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: blue[700]}}> <Typography variant="body2" style={{ color: blue[700]}}>
{device.status.sen5x?.humidity.toFixed(1)}% Humidity: {device.status.sen5x?.humidity.toFixed(1)}%
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: blue[300]}}>
Voc: {device.status.sen5x?.voc.toFixed(1)}%
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: blue[400]}}>
Nox: {device.status.sen5x?.nox.toFixed(1)}%
</Typography> </Typography>
</Grid2> </Grid2>
</Grid2> </Grid2>
<Grid2 container direction="column" {!noDust && <Grid2 container direction="column"
className={`${classes.carouselItem} ${ className={`${classes.carouselItem} ${
1 === currentIndex ? classes.active : classes.inactive 1 === currentIndex ? classes.active : classes.inactive
}`} }`}
> >
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: green[300]}}> <Typography variant="body2" style={{ color: green[300]}}>
{device.status.sen5x?.dust1ug.toFixed(1)}ug/m3 {"<1um:"} {device.status.sen5x?.dust1ug.toFixed(1)}ug/m3
</Typography> </Typography>
</Grid2> </Grid2>
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: green[400]}}> <Typography variant="body2" style={{ color: green[400]}}>
{device.status.sen5x?.dust2ug.toFixed(1)}ug/m3 {"<2.5um:"} {device.status.sen5x?.dust2ug.toFixed(1)}ug/m3
</Typography> </Typography>
</Grid2> </Grid2>
</Grid2>
<Grid2 container direction="column"
className={`${classes.carouselItem} ${
2 === currentIndex ? classes.active : classes.inactive
}`}
>
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: green[500]}}> <Typography variant="body2" style={{ color: green[500]}}>
{device.status.sen5x?.dust4ug.toFixed(1)}ug/m3 {"<4um: "}{device.status.sen5x?.dust4ug.toFixed(1)}ug/m3
</Typography> </Typography>
</Grid2> </Grid2>
<Grid2> <Grid2>
<Typography variant="body2" style={{ color: green[600]}}> <Typography variant="body2" style={{ color: green[600]}}>
{device.status.sen5x?.dust10ug.toFixed(1)}ug/m3 {"<10um: "}{device.status.sen5x?.dust10ug.toFixed(1)}ug/m3
</Typography> </Typography>
</Grid2> </Grid2>
</Grid2> </Grid2>}
<Grid2 container direction="column"
className={`${classes.carouselItem} ${
3 === currentIndex ? classes.active : classes.inactive
}`}
>
<Grid2>
<Typography variant="body2" style={{ color: blue[300]}}>
V: {device.status.sen5x?.voc.toFixed(1)}%
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: blue[400]}}>
N: {device.status.sen5x?.nox.toFixed(1)}%
</Typography>
</Grid2>
</Grid2>
</Grid2> </Grid2>
</PulseBox> </PulseBox>
</Tooltip> </Tooltip>

View file

@ -17,7 +17,7 @@ import { isController, isSource } from "pbHelpers/ComponentType";
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability"; import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { or } from "utils/types"; import { or } from "utils/types";
import ComponentSettings from "./ComponentSettings"; import ComponentSettings from "./ComponentSettings";
import { green, teal } from "@mui/material/colors"; import { green, teal } from "@mui/material/colors";
@ -94,6 +94,7 @@ export default function ComponentActions(props: Props) {
}; };
const openComponentSettingsDialog = (mode: string) => { const openComponentSettingsDialog = (mode: string) => {
console.log("delete")
setIsComponentSettingsOpen(true); setIsComponentSettingsOpen(true);
setComponentSettingsMode(or(mode, "")); setComponentSettingsMode(or(mode, ""));
}; };

View file

@ -14,7 +14,6 @@ import { pond, quack } from "protobuf-ts/pond";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import DeviceOverview from "device/DeviceOverview"; import DeviceOverview from "device/DeviceOverview";
import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability"; import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import AddComponentManualDialog from "component/AddComponentManualDialog";
import ComponentCard from "component/ComponentCard"; import ComponentCard from "component/ComponentCard";
import { sameComponentID, sortComponents } from "pbHelpers/Component"; import { sameComponentID, sortComponents } from "pbHelpers/Component";
import { isController } from "pbHelpers/ComponentType"; import { isController } from "pbHelpers/ComponentType";

View file

@ -1,5 +1,5 @@
import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon } from "@mui/icons-material"; import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon, Tune } from "@mui/icons-material";
import { Box, Chip, Grid2, IconButton, Skeleton, Tab, Tabs, Theme, Tooltip, Typography, useTheme } from "@mui/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 { blue, green, orange } from "@mui/material/colors";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ResponsiveTable, { Column } from "common/ResponsiveTable";
@ -24,6 +24,8 @@ import { Tag as TagUI } from "common/Tag";
import moment from "moment"; import moment from "moment";
import StatusPlenum from "common/StatusPlenum"; import StatusPlenum from "common/StatusPlenum";
import StatusSen5x from "common/StatusSen5x"; import StatusSen5x from "common/StatusSen5x";
import StatusDust from "common/StatusDust";
import StatusGas from "common/StatusGas";
const useStyles = makeStyles((theme: Theme) => { const useStyles = makeStyles((theme: Theme) => {
return ({ return ({
@ -100,6 +102,10 @@ export default function Devices() {
const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false); const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState<boolean>(false);
const [hasPlenums, setHasPlenums] = useState(false) const [hasPlenums, setHasPlenums] = useState(false)
const [hasSen5x, setHasSen5x] = 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 [groupsLoading, setGroupsLoading] = useState(false)
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
@ -122,6 +128,26 @@ export default function Devices() {
const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState<boolean>(false); const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState<boolean>(false);
const [fieldContains, setFieldContains] = useState<Map<string, string>>(new Map<string, string>()) 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 [{ as }] = useGlobalState()
@ -250,12 +276,12 @@ export default function Devices() {
).then(resp => { ).then(resp => {
let newDevices: Device[] = [] let newDevices: Device[] = []
resp.data.devices.forEach(device => { resp.data.devices.forEach(device => {
if (device.status?.plenum?.temperature) { if (device.status?.plenum?.temperature) setHasPlenums(true)
setHasPlenums(true) if (device.status?.sen5x?.temperature) setHasSen5x(true)
} if (device.status?.o2) setHasO2(true)
if (device.status?.sen5x?.temperature) { if (device.status?.co) setHasCo(true)
setHasSen5x(true) if (device.status?.co2) setHasCo2(true)
} if (device.status?.no2) setHasNo2(true)
newDevices.push(Device.create(device)) newDevices.push(Device.create(device))
}) })
setDevices(newDevices) setDevices(newDevices)
@ -277,15 +303,40 @@ export default function Devices() {
setLimit(event.target.value); setLimit(event.target.value);
}; };
const prependToUrl = (prependage: number | string) => { // const prependToUrl = (prependage: number | string) => {
const basePath = location.pathname.replace(/\/$/, ""); // const basePath = location.pathname.replace(/\/$/, "");
return(`/${prependage}/${basePath}`.replace(/\/{2,}/g, "/").replace(/(\/[^/]+)\/\1+/g, "$1")); // 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) => { const toDevice = (device: Device) => {
let url = prependToUrl(getGroup() ? "groups/" + getGroup().id() : "") let url = getGroup() ? insertGroupContext(getGroup().id().toString(), device.id().toString()) : ""
url = url + "/" + device.id() if (url.length < 1) url = device.id().toString()
navigate(url, { replace: true, state: {device: device} }) navigate(url, { state: {device: device} })
}; };
const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => { const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => {
@ -396,7 +447,95 @@ export default function Devices() {
if (device.status.sen5x?.temperature) { if (device.status.sen5x?.temperature) {
return ( return (
<Box sx={{ marginLeft: 1 }}> <Box sx={{ marginLeft: 1 }}>
<StatusSen5x device={device} /> <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> </Box>
) )
} else { } else {
@ -499,6 +638,51 @@ export default function Devices() {
) )
} }
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>
)
}
return( return(
<PageContainer padding={isMobile ? 0 : 2}> <PageContainer padding={isMobile ? 0 : 2}>
<Box <Box
@ -548,6 +732,7 @@ export default function Devices() {
setOrder={setOrder} setOrder={setOrder}
orderBy={orderBy} orderBy={orderBy}
setOrderBy={setOrderBy} setOrderBy={setOrderBy}
endTitleElement={preferencesButton}
/> />
<ProvisionDevice <ProvisionDevice
isOpen={isProvisionDialogOpen} isOpen={isProvisionDialogOpen}
@ -566,6 +751,7 @@ export default function Devices() {
removeGroupCallback={removeGroupCallback} removeGroupCallback={removeGroupCallback}
addGroupCallback={addGroupCallback} addGroupCallback={addGroupCallback}
/> />
{preferencesMenu()}
</PageContainer> </PageContainer>
) )
} }

View file

@ -1,14 +1,9 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
// import { useAuth } from "hooks";
import moment from "moment"; import moment from "moment";
import { createContext, PropsWithChildren, useContext } from "react"; import { createContext, PropsWithChildren, useContext } from "react";
// import BillingProvider from "./billing";
// import GitlabProvider from "./gitlab";
import PondProvider from "./pond/pond"; import PondProvider from "./pond/pond";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuth0 } from "@auth0/auth0-react";
import SnackbarProvider from "./Snackbar"; import SnackbarProvider from "./Snackbar";
// import SecurityProvider from "./security";
// import { useAuth0 } from "@auth0/auth0-react";
interface IHTTPContext { interface IHTTPContext {
get: <T>(url: string, spreadOptions?: AxiosRequestConfig) => Promise<AxiosResponse<T>>; get: <T>(url: string, spreadOptions?: AxiosRequestConfig) => Promise<AxiosResponse<T>>;
@ -78,9 +73,7 @@ export default function HTTPProvider(props: Props) {
} }
function get<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> { function get<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
if (isTokenExpired(token)) { if (isTokenExpired(token)) loginWithRedirect()
loginWithRedirect()
}
return axios.get(url, {...defaultOptions(), ...spreadOptions}); return axios.get(url, {...defaultOptions(), ...spreadOptions});
} }