merged gas status branch
This commit is contained in:
commit
fa78c29717
10 changed files with 596 additions and 70 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -42,7 +42,7 @@
|
|||
"mui-tel-input": "^7.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"openweathermap-ts": "^1.2.10",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
|
|
@ -10864,7 +10864,7 @@
|
|||
},
|
||||
"node_modules/protobuf-ts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#f7d57e5a1a880fcea3628bbc10889c75314216e4",
|
||||
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#cead0be8708fa2c6679c77ed38f51239341c4ed4",
|
||||
"dependencies": {
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
"mui-tel-input": "^7.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"openweathermap-ts": "^1.2.10",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#dev",
|
||||
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ interface Props<T> {
|
|||
setOrderBy?: React.Dispatch<React.SetStateAction<string>>;
|
||||
order?: string;
|
||||
setOrder?: React.Dispatch<React.SetStateAction<"asc" | "desc">>;
|
||||
endTitleElement?: string | JSX.Element | (() => string | JSX.Element);
|
||||
}
|
||||
|
||||
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 title = typeof(props.title) === "function" ? props.title() : props.title
|
||||
// const order = props.order ? props.order : "asc"
|
||||
const endTitleElement = typeof(props.endTitleElement) === "function" ? props.endTitleElement() : props.endTitleElement
|
||||
|
||||
const isMobile = useMobile()
|
||||
|
||||
|
|
@ -318,11 +320,13 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
|
||||
if (isMobile || mobileView) return (
|
||||
<Box>
|
||||
|
||||
<Box className={classes.mobileTitleBox}>
|
||||
{renderTitle()}
|
||||
{renderSubtitle()}
|
||||
{setSearchText && searchBar()}
|
||||
</Box>
|
||||
|
||||
{rows.map((row, index) => {
|
||||
return (
|
||||
<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>
|
||||
{actions && actions}
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
{endTitleElement}
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
{setSearchText &&<Box className={classes.searchContainer}>
|
||||
{searchBar()}
|
||||
|
|
|
|||
149
src/common/StatusDust.tsx
Normal file
149
src/common/StatusDust.tsx
Normal 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
199
src/common/StatusGas.tsx
Normal 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()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import { Device } from "models";
|
|||
import PulseBox from "./PulseBox";
|
||||
import { or } from "utils";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
device: Device
|
||||
noDust?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
|
|
@ -19,8 +21,8 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
padding: theme.spacing(0.75),
|
||||
marginRight: "auto",
|
||||
margin: theme.spacing(1),
|
||||
width: "72px",
|
||||
height: "50px",
|
||||
width: theme.spacing(16),
|
||||
height: theme.spacing(11),
|
||||
},
|
||||
carouselContainer: {
|
||||
position: 'relative',
|
||||
|
|
@ -47,10 +49,10 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
})
|
||||
})
|
||||
|
||||
export default function StatusPlenum(props: Props) {
|
||||
const { device } = props;
|
||||
export default function StatusSen5x(props: Props) {
|
||||
const { device, noDust } = props;
|
||||
const classes = useStyles()
|
||||
|
||||
// console.log(noDust)
|
||||
const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []);
|
||||
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);
|
||||
|
||||
// Auto-cycle through measurements every 3 seconds
|
||||
// Auto-cycle through measurements every 5 seconds
|
||||
useEffect(() => {
|
||||
if (noDust) {
|
||||
setCurrentIndex(0)
|
||||
return
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % 4);
|
||||
}, 3000); // Adjust timing as needed (3000ms = 3s)
|
||||
if (!noDust) setCurrentIndex((prevIndex) => (prevIndex + 1) % 2);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval); // Cleanup on unmount
|
||||
}, []);
|
||||
}, [noDust]);
|
||||
|
||||
const tooltip = () => {
|
||||
if (messages.length === 0) return null
|
||||
|
|
@ -125,66 +131,52 @@ export default function StatusPlenum(props: Props) {
|
|||
>
|
||||
<Grid2>
|
||||
<Typography variant="body2" style={{ color: orange[700]}}>
|
||||
{device.status.sen5x?.temperature.toFixed(1)}°C
|
||||
Temp: {device.status.sen5x?.temperature.toFixed(1)}°C
|
||||
</Typography>
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
<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>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
||||
<Grid2 container direction="column"
|
||||
{!noDust && <Grid2 container direction="column"
|
||||
className={`${classes.carouselItem} ${
|
||||
1 === currentIndex ? classes.active : classes.inactive
|
||||
}`}
|
||||
>
|
||||
<Grid2>
|
||||
<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>
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
<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>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
|
||||
<Grid2 container direction="column"
|
||||
className={`${classes.carouselItem} ${
|
||||
2 === currentIndex ? classes.active : classes.inactive
|
||||
}`}
|
||||
>
|
||||
<Grid2>
|
||||
<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>
|
||||
</Grid2>
|
||||
<Grid2>
|
||||
<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>
|
||||
</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>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { isController, isSource } from "pbHelpers/ComponentType";
|
|||
import { DeviceAvailabilityMap, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useGlobalState } from "providers";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { or } from "utils/types";
|
||||
import ComponentSettings from "./ComponentSettings";
|
||||
import { green, teal } from "@mui/material/colors";
|
||||
|
|
@ -94,6 +94,7 @@ export default function ComponentActions(props: Props) {
|
|||
};
|
||||
|
||||
const openComponentSettingsDialog = (mode: string) => {
|
||||
console.log("delete")
|
||||
setIsComponentSettingsOpen(true);
|
||||
setComponentSettingsMode(or(mode, ""));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { pond, quack } from "protobuf-ts/pond";
|
|||
import { cloneDeep } from "lodash";
|
||||
import DeviceOverview from "device/DeviceOverview";
|
||||
import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
|
||||
import AddComponentManualDialog from "component/AddComponentManualDialog";
|
||||
import ComponentCard from "component/ComponentCard";
|
||||
import { sameComponentID, sortComponents } from "pbHelpers/Component";
|
||||
import { isController } from "pbHelpers/ComponentType";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { 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";
|
||||
|
|
@ -24,6 +24,8 @@ 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";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
return ({
|
||||
|
|
@ -100,6 +102,10 @@ export default function Devices() {
|
|||
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[]>([]);
|
||||
|
|
@ -123,6 +129,26 @@ export default function Devices() {
|
|||
|
||||
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[]) => {
|
||||
|
|
@ -250,12 +276,12 @@ export default function Devices() {
|
|||
).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?.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)
|
||||
|
|
@ -277,15 +303,40 @@ export default function Devices() {
|
|||
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 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 = prependToUrl(getGroup() ? "groups/" + getGroup().id() : "")
|
||||
url = url + "/" + device.id()
|
||||
navigate(url, { replace: true, state: {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) => {
|
||||
|
|
@ -396,7 +447,95 @@ export default function Devices() {
|
|||
if (device.status.sen5x?.temperature) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
} 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(
|
||||
<PageContainer padding={isMobile ? 0 : 2}>
|
||||
<Box
|
||||
|
|
@ -548,6 +732,7 @@ export default function Devices() {
|
|||
setOrder={setOrder}
|
||||
orderBy={orderBy}
|
||||
setOrderBy={setOrderBy}
|
||||
endTitleElement={preferencesButton}
|
||||
/>
|
||||
<ProvisionDevice
|
||||
isOpen={isProvisionDialogOpen}
|
||||
|
|
@ -566,6 +751,7 @@ export default function Devices() {
|
|||
removeGroupCallback={removeGroupCallback}
|
||||
addGroupCallback={addGroupCallback}
|
||||
/>
|
||||
{preferencesMenu()}
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
// import { useAuth } from "hooks";
|
||||
import moment from "moment";
|
||||
import { createContext, PropsWithChildren, useContext } from "react";
|
||||
// import BillingProvider from "./billing";
|
||||
// import GitlabProvider from "./gitlab";
|
||||
import PondProvider from "./pond/pond";
|
||||
import { useAuth0 } from "@auth0/auth0-react";
|
||||
import SnackbarProvider from "./Snackbar";
|
||||
// import SecurityProvider from "./security";
|
||||
// import { useAuth0 } from "@auth0/auth0-react";
|
||||
|
||||
interface IHTTPContext {
|
||||
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>> {
|
||||
if (isTokenExpired(token)) {
|
||||
loginWithRedirect()
|
||||
}
|
||||
if (isTokenExpired(token)) loginWithRedirect()
|
||||
return axios.get(url, {...defaultOptions(), ...spreadOptions});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue