From ebf6bdb71a2d2765b0e7fd1d1d4b15cd01de150d Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 16 May 2025 10:53:26 -0600 Subject: [PATCH 1/8] fixed navigation bug in devices page; added option to seperate dust from sen5x to different column --- src/common/StatusSen5x.tsx | 61 +++++++++++---------------- src/component/ComponentActions.tsx | 3 +- src/pages/Device.tsx | 1 - src/pages/Devices.tsx | 68 ++++++++++++++++++++++++------ src/providers/http.tsx | 9 +--- 5 files changed, 82 insertions(+), 60 deletions(-) diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 912f128..887fd10 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -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', @@ -48,7 +50,7 @@ const useStyles = makeStyles((theme: Theme) => { }) export default function StatusPlenum(props: Props) { - const { device } = props; + const { device, noDust } = props; const classes = useStyles() const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []); @@ -73,8 +75,9 @@ export default function StatusPlenum(props: Props) { // Auto-cycle through measurements every 3 seconds useEffect(() => { + if (noDust) return const interval = setInterval(() => { - setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); + setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); }, 3000); // Adjust timing as needed (3000ms = 3s) return () => clearInterval(interval); // Cleanup on unmount @@ -125,66 +128,52 @@ export default function StatusPlenum(props: Props) { > - {device.status.sen5x?.temperature.toFixed(1)}°C + Temp: {device.status.sen5x?.temperature.toFixed(1)}°C - {device.status.sen5x?.humidity.toFixed(1)}% + Humidity: {device.status.sen5x?.humidity.toFixed(1)}% + + + + + Voc: {device.status.sen5x?.voc.toFixed(1)}% + + + + + Nox: {device.status.sen5x?.nox.toFixed(1)}% - - {device.status.sen5x?.dust1ug.toFixed(1)}ug/m3 + {"<1um:"} {device.status.sen5x?.dust1ug.toFixed(1)}ug/m3 - {device.status.sen5x?.dust2ug.toFixed(1)}ug/m3 + {"<2.5um:"} {device.status.sen5x?.dust2ug.toFixed(1)}ug/m3 - - - - {device.status.sen5x?.dust4ug.toFixed(1)}ug/m3 + {"<4um: "}{device.status.sen5x?.dust4ug.toFixed(1)}ug/m3 - {device.status.sen5x?.dust10ug.toFixed(1)}ug/m3 + {"<10um: "}{device.status.sen5x?.dust10ug.toFixed(1)}ug/m3 - - - - - - V: {device.status.sen5x?.voc.toFixed(1)}% - - - - - N: {device.status.sen5x?.nox.toFixed(1)}% - - - + } diff --git a/src/component/ComponentActions.tsx b/src/component/ComponentActions.tsx index f633240..3d1ebc9 100644 --- a/src/component/ComponentActions.tsx +++ b/src/component/ComponentActions.tsx @@ -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, "")); }; diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 911e9f0..eabaf80 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -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"; diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index c4239c5..21edc95 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -24,6 +24,7 @@ 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"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -122,6 +123,7 @@ export default function Devices() { const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState(false); const [fieldContains, setFieldContains] = useState>(new Map()) + const [seperateDust, setSeperateDust] = useState(true) const [{ as }] = useGlobalState() @@ -250,12 +252,8 @@ 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) newDevices.push(Device.create(device)) }) setDevices(newDevices) @@ -277,15 +275,41 @@ 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 + console.log(path) + 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('/'); + } 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()) : "" + console.log(url) + if (url.length < 1) url = device.id().toString() + // url = url + "/" + device.id() + // navigate(url, { replace: true, state: {device: device} }) + navigate(url, { state: {device: device} }) }; const handleTabChange = (_event: React.SyntheticEvent, newValue: string) => { @@ -384,7 +408,23 @@ export default function Devices() { if (device.status.sen5x?.temperature) { return ( - + + + ) + } else { + return ( + <> + ) + } + } + }) + if (seperateDust) columns.push({ + title: "Dust", + render: (device: Device) => { + if (device.status.sen5x?.temperature) { + return ( + + ) } else { diff --git a/src/providers/http.tsx b/src/providers/http.tsx index 6811355..c4d9f64 100644 --- a/src/providers/http.tsx +++ b/src/providers/http.tsx @@ -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: (url: string, spreadOptions?: AxiosRequestConfig) => Promise>; @@ -78,9 +73,7 @@ export default function HTTPProvider(props: Props) { } function get(url: string, spreadOptions?: AxiosRequestConfig): Promise> { - if (isTokenExpired(token)) { - loginWithRedirect() - } + if (isTokenExpired(token)) loginWithRedirect() return axios.get(url, {...defaultOptions(), ...spreadOptions}); } From 33988e368466a3f2917df98fc8455c5b2b942422 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 16 May 2025 11:33:25 -0600 Subject: [PATCH 2/8] added optional element to responsive table to render an element before the search bar --- src/common/ResponsiveTable.tsx | 11 ++++++++--- src/pages/Devices.tsx | 35 ++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index de23f1b..dea576a 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -92,6 +92,7 @@ interface Props { customElement?: JSX.Element //element that will be placed in the table head between the table actions and the column header rows mobileView?: boolean //can be used to force the table to use its mobile view for narrow containing elements ie, drawer hidePagination?: boolean //when passed in will hide the pagination at the bottom of the table + endTitleElement?: string | JSX.Element | (() => string | JSX.Element); } export default function ResponsiveTable(props: Props) { @@ -126,6 +127,7 @@ export default function ResponsiveTable(props: Props) { const columns = typeof(props.columns) === "function" ? props.columns() : props.columns const subtitle = typeof(props.subtitle) === "function" ? props.subtitle() : props.subtitle const title = typeof(props.title) === "function" ? props.title() : props.title + const endTitleElement = typeof(props.endTitleElement) === "function" ? props.endTitleElement() : props.endTitleElement const isMobile = useMobile() @@ -307,11 +309,13 @@ export default function ResponsiveTable(props: Props) { if (isMobile || mobileView) return ( + {renderTitle()} {renderSubtitle()} {setSearchText && searchBar()} + {rows.map((row, index) => { return ( onRowClick&&onRowClick(row)}> @@ -375,10 +379,9 @@ export default function ResponsiveTable(props: Props) { return ( - + - @@ -393,6 +396,9 @@ export default function ResponsiveTable(props: Props) { {actions && actions} + + {endTitleElement} + {setSearchText && {searchBar()} @@ -418,7 +424,6 @@ export default function ResponsiveTable(props: Props) { } - { rowSelect && } diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 21edc95..d4518fe 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -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, Chip, Dialog, DialogTitle, Grid2, IconButton, 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"; @@ -125,6 +125,8 @@ export default function Devices() { const [fieldContains, setFieldContains] = useState>(new Map()) const [seperateDust, setSeperateDust] = useState(true) + const [preferencesAnchor, setPreferencesAnchor] = useState(null) + const [{ as }] = useGlobalState() const updateGroups = (newGroups: Group[]) => { @@ -282,7 +284,6 @@ export default function Devices() { function insertGroupContext(groupID: string, deviceID: string): string { const path = location.pathname - console.log(path) const segments = path.split('/').filter(Boolean); const deviceIndex = segments.findIndex(segment => segment === 'devices'); @@ -305,10 +306,7 @@ export default function Devices() { const toDevice = (device: Device) => { let url = getGroup() ? insertGroupContext(getGroup().id().toString(), device.id().toString()) : "" - console.log(url) if (url.length < 1) url = device.id().toString() - // url = url + "/" + device.id() - // navigate(url, { replace: true, state: {device: device} }) navigate(url, { state: {device: device} }) }; @@ -527,6 +525,29 @@ export default function Devices() { ) } + const preferencesButton = () => { + return ( + setPreferencesAnchor(event.currentTarget)}> + + + ) + } + + const preferencesMenu = () => { + return ( + setPreferencesAnchor(null)} + disableAutoFocusItem> + + hi + + + ) + } + return( } + endTitleElement={preferencesButton} /> + {preferencesMenu()} ) } \ No newline at end of file From c19974edd9109d1cec66751f261e83266a8e4229 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 16 May 2025 11:58:22 -0600 Subject: [PATCH 3/8] made a preferences element to group/ungroup sen5x stuff --- src/pages/Devices.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index d4518fe..562ca25 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -1,5 +1,5 @@ import { DeveloperBoard as ProvisionIcon, LibraryAdd as CreateGroupIcon, Tune } from "@mui/icons-material"; -import { Box, Chip, Dialog, DialogTitle, Grid2, IconButton, Menu, MenuItem, 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 { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; @@ -123,7 +123,7 @@ export default function Devices() { const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState(false); const [fieldContains, setFieldContains] = useState>(new Map()) - const [seperateDust, setSeperateDust] = useState(true) + const [seperateDust, setSeperateDust] = useState(false) const [preferencesAnchor, setPreferencesAnchor] = useState(null) @@ -533,6 +533,8 @@ export default function Devices() { ) } + + const preferencesMenu = () => { return ( setPreferencesAnchor(null)} disableAutoFocusItem> - - hi + setSeperateDust(!seperateDust)} + sx={{ marginLeft: -0.75 }} + dense + > + + + + ) From 2a578a947e36a019b7594e12952771c61aaf0527 Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 16 May 2025 12:27:22 -0600 Subject: [PATCH 4/8] fixed sen5x status component not rotating after switching setting --- src/common/StatusSen5x.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 887fd10..632709e 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -52,7 +52,7 @@ const useStyles = makeStyles((theme: Theme) => { export default function StatusPlenum(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), []); @@ -73,15 +73,19 @@ 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) return + if (noDust) { + setCurrentIndex(0) + return + } const interval = setInterval(() => { - setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); - }, 3000); // Adjust timing as needed (3000ms = 3s) + console.log(noDust) + if (!noDust) setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); + }, 5000); return () => clearInterval(interval); // Cleanup on unmount - }, []); + }, [noDust]); const tooltip = () => { if (messages.length === 0) return null From abde5c6499ed74487219ffff5fa19afe60aaa96f Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 16 May 2025 13:26:17 -0600 Subject: [PATCH 5/8] separateDust setting will persist in local storage --- src/common/StatusDust.tsx | 149 +++++++++++++++++++++++++++++++++++++ src/common/StatusSen5x.tsx | 1 - src/pages/Devices.tsx | 19 +++-- 3 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 src/common/StatusDust.tsx diff --git a/src/common/StatusDust.tsx b/src/common/StatusDust.tsx new file mode 100644 index 0000000..2333318 --- /dev/null +++ b/src/common/StatusDust.tsx @@ -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 ( + + + + Overlay Messages + + + {messages.map((message, index) => { + return ( + + + + {message} + + + ) + })} + + ) + } + + return ( + + + + + + + {"<1um:"} {device.status.sen5x?.dust1ug.toFixed(1)}ug/m3 + + + + + {"<2.5um:"} {device.status.sen5x?.dust2ug.toFixed(1)}ug/m3 + + + + + {"<4um: "}{device.status.sen5x?.dust4ug.toFixed(1)}ug/m3 + + + + + {"<10um: "}{device.status.sen5x?.dust10ug.toFixed(1)}ug/m3 + + + + + + + ) +} \ No newline at end of file diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 632709e..8d32f35 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -80,7 +80,6 @@ export default function StatusPlenum(props: Props) { return } const interval = setInterval(() => { - console.log(noDust) if (!noDust) setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); }, 5000); diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index 562ca25..a394d18 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -123,7 +123,15 @@ export default function Devices() { const [groupSettingsIsOpen, setGroupSettingsIsOpen] = useState(false); const [fieldContains, setFieldContains] = useState>(new Map()) - const [seperateDust, setSeperateDust] = useState(false) + + const [separateDust, setSeparateDust] = useState(() => { + const stored = localStorage.getItem('separateDust'); + return stored !== null ? stored === 'true' : false; + }); + + useEffect(() => { + localStorage.setItem('separateDust', separateDust.toString()); + }, [separateDust]); const [preferencesAnchor, setPreferencesAnchor] = useState(null) @@ -304,7 +312,6 @@ export default function 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} }) @@ -406,7 +413,7 @@ export default function Devices() { if (device.status.sen5x?.temperature) { return ( - + ) } else { @@ -416,7 +423,7 @@ export default function Devices() { } } }) - if (seperateDust) columns.push({ + if (separateDust) columns.push({ title: "Dust", render: (device: Device) => { if (device.status.sen5x?.temperature) { @@ -544,13 +551,13 @@ export default function Devices() { onClose={() => setPreferencesAnchor(null)} disableAutoFocusItem> setSeperateDust(!seperateDust)} + onClick={() => setSeparateDust(!separateDust)} sx={{ marginLeft: -0.75 }} dense > From 6dc181b24cccdc087a83cbcefdef3e4c5b5f98b2 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 21 May 2025 13:49:00 -0600 Subject: [PATCH 6/8] status card for gas readings --- package-lock.json | 4 +- package.json | 2 +- src/common/StatusGas.tsx | 162 +++++++++++++++++++++++++++++++++++++ src/common/StatusSen5x.tsx | 2 +- src/pages/Devices.tsx | 85 +++++++++++++++++++ 5 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 src/common/StatusGas.tsx diff --git a/package-lock.json b/package-lock.json index e369745..a656312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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#master", + "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#f8704561c094b208029340cdf2ce0c510ef1eb12", + "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#cead0be8708fa2c6679c77ed38f51239341c4ed4", "dependencies": { "protobufjs": "^6.8.8" } diff --git a/package.json b/package.json index 8e3ebd3..5a32392 100644 --- a/package.json +++ b/package.json @@ -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#master", + "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", diff --git a/src/common/StatusGas.tsx b/src/common/StatusGas.tsx new file mode 100644 index 0000000..b0ebb85 --- /dev/null +++ b/src/common/StatusGas.tsx @@ -0,0 +1,162 @@ +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"; + +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), + }, + 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) + // 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 5 seconds + // useEffect(() => { + // if (noDust) { + // setCurrentIndex(0) + // return + // } + // const interval = setInterval(() => { + // if (!noDust) setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); + // setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); + // }, 5000); + + // return () => clearInterval(interval); // Cleanup on unmount + // }, [noDust]); + + // const tooltip = () => { + // if (messages.length === 0) return null + // if (messages.length < 2) return ( + // messages[0] + // ) + // return ( + // + // + // + // Overlay Messages + // + // + // {messages.map((message, index) => { + // return ( + // + // + // + // {message} + // + // + // ) + // })} + // + // ) + // } + + const gasDisplay = () => { + if (gasType !== "all") { + return( + // + + + + + {gasType !== "o2" ? + + Gas: {device.status[gasType]?.ppm.toFixed(1)}ppm + + : + + Gas: {(device.status[gasType]?.ppm!/10000).toFixed(1)}% + + } + + + + Volt: {device.status[gasType]?.millivolts.toFixed(1)}mV + + + + + + // + ) + + } + } + + return ( + <> + {gasDisplay()} + + ) +} \ No newline at end of file diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 8d32f35..288eb10 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -49,7 +49,7 @@ const useStyles = makeStyles((theme: Theme) => { }) }) -export default function StatusPlenum(props: Props) { +export default function StatusSen5x(props: Props) { const { device, noDust } = props; const classes = useStyles() // console.log(noDust) diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index a394d18..dcd2c1c 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -25,6 +25,7 @@ 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 ({ @@ -101,6 +102,10 @@ export default function Devices() { const [isProvisionDialogOpen, setIsProvisionDialogOpen] = useState(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([]); @@ -264,6 +269,10 @@ export default function Devices() { 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) @@ -311,6 +320,10 @@ export default function Devices() { 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() @@ -440,6 +453,78 @@ export default function Devices() { } }) } + if (hasCo) { + columns.push({ + title: "CO", + render: (device: Device) => { + if (device.status.co) { + return ( + + + + ) + } else { + return ( + <> + ) + } + } + }) + } + if (hasCo2) { + columns.push({ + title: "CO2", + render: (device: Device) => { + if (device.status.co2) { + return ( + + + + ) + } else { + return ( + <> + ) + } + } + }) + } + if (hasNo2) { + columns.push({ + title: "NO2", + render: (device: Device) => { + if (device.status.no2) { + return ( + + + + ) + } else { + return ( + <> + ) + } + } + }) + } + if (hasCo2) { + columns.push({ + title: "O2", + render: (device: Device) => { + if (device.status.o2) { + return ( + + + + ) + } else { + return ( + <> + ) + } + } + }) + } return columns } From 32c2fbdb6e2be17e07a2c79c9d7f812b1aec82dc Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 23 May 2025 12:13:10 -0600 Subject: [PATCH 7/8] StatusGas now displays overlays and can be grouped --- src/common/StatusGas.tsx | 172 ++++++++++++++++++++------------------- src/pages/Devices.tsx | 44 +++++++--- 2 files changed, 120 insertions(+), 96 deletions(-) diff --git a/src/common/StatusGas.tsx b/src/common/StatusGas.tsx index b0ebb85..a714ba5 100644 --- a/src/common/StatusGas.tsx +++ b/src/common/StatusGas.tsx @@ -5,6 +5,7 @@ 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 @@ -54,104 +55,107 @@ export default function StatusGas(props: Props) { const { device, gasType } = 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), []); + const colors = or(device.status.o2?.overlays.map(overlay => overlay.color), []); + const messages = or(device.status.o2?.overlays.map(overlay => overlay.message), []); - // const [color, setColor] = useState(colors.length > 0 ? colors[0] : ""); - // const [, setColorIndex] = useState(0) + 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); + 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); - // }, []); + return () => clearInterval(interval); + }, []); - // const [currentIndex, setCurrentIndex] = useState(0); + const [currentIndex, setCurrentIndex] = useState(0); // Auto-cycle through measurements every 5 seconds - // useEffect(() => { - // if (noDust) { - // setCurrentIndex(0) - // return - // } - // const interval = setInterval(() => { - // if (!noDust) setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); - // setCurrentIndex((prevIndex) => (prevIndex + 1) % 2); - // }, 5000); + useEffect(() => { + if (gasType !== "all") { + setCurrentIndex(0) + return + } + const interval = setInterval(() => { + if (gasType === "all") setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); + setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); + }, 5000); - // return () => clearInterval(interval); // Cleanup on unmount - // }, [noDust]); + return () => clearInterval(interval); // Cleanup on unmount + }, [gasType]); - // const tooltip = () => { - // if (messages.length === 0) return null - // if (messages.length < 2) return ( - // messages[0] - // ) - // return ( - // - // - // - // Overlay Messages - // - // - // {messages.map((message, index) => { - // return ( - // - // - // - // {message} - // - // - // ) - // })} - // - // ) - // } + const tooltip = () => { + if (messages.length === 0) return null + if (messages.length < 2) return ( + messages[0] + ) + return ( + + + + Overlay Messages + + + {messages.map((message, index) => { + return ( + + + + {message} + + + ) + })} + + ) + } const gasDisplay = () => { - if (gasType !== "all") { - return( - // - - - - - {gasType !== "o2" ? - - Gas: {device.status[gasType]?.ppm.toFixed(1)}ppm - - : - - Gas: {(device.status[gasType]?.ppm!/10000).toFixed(1)}% - - } - - - - Volt: {device.status[gasType]?.millivolts.toFixed(1)}mV + const gas = gasType === "all" ? gasTypes[currentIndex] : gasType + // if (gasType !== "all") { + return ( + + + + + + {gas !== "o2" ? + + Gas: {device.status[gas]?.ppm.toFixed(1)}ppm - + : + + Gas: {(device.status[gas]?.ppm!/10000).toFixed(1)}% + + } + + + + Volt: {device.status[gas]?.millivolts.toFixed(1)}mV + - - // - ) + + + + ) - } + // } } return ( diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index dcd2c1c..bc43773 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -138,6 +138,15 @@ export default function Devices() { 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(null) const [{ as }] = useGlobalState() @@ -320,9 +329,9 @@ export default function Devices() { return '/' + newSegments.join('/'); } - useEffect(() => { - console.log(devices) - }, [devices]) + // useEffect(() => { + // console.log(devices) + // }, [devices]) const toDevice = (device: Device) => { let url = getGroup() ? insertGroupContext(getGroup().id().toString(), device.id().toString()) : "" @@ -455,12 +464,12 @@ export default function Devices() { } if (hasCo) { columns.push({ - title: "CO", + title: groupGas ? "Gas" : "CO", render: (device: Device) => { if (device.status.co) { return ( - + ) } else { @@ -471,7 +480,7 @@ export default function Devices() { } }) } - if (hasCo2) { + if (hasCo2 && !groupGas) { columns.push({ title: "CO2", render: (device: Device) => { @@ -489,7 +498,7 @@ export default function Devices() { } }) } - if (hasNo2) { + if (hasNo2 && !groupGas) { columns.push({ title: "NO2", render: (device: Device) => { @@ -507,7 +516,7 @@ export default function Devices() { } }) } - if (hasCo2) { + if (hasO2 && !groupGas) { columns.push({ title: "O2", render: (device: Device) => { @@ -625,8 +634,6 @@ export default function Devices() { ) } - - const preferencesMenu = () => { return ( - + + + setGroupGas(!groupGas)} + sx={{ marginLeft: -0.75 }} + dense + > + + + + ) From 7d1081ea505947d7b0385809aa011555c171e05f Mon Sep 17 00:00:00 2001 From: Carter Date: Fri, 23 May 2025 13:13:37 -0600 Subject: [PATCH 8/8] gas status now rotates through overlays properly, shows the correct readings when grouped --- src/common/StatusGas.tsx | 69 +++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/src/common/StatusGas.tsx b/src/common/StatusGas.tsx index a714ba5..dd9737f 100644 --- a/src/common/StatusGas.tsx +++ b/src/common/StatusGas.tsx @@ -26,6 +26,16 @@ const useStyles = makeStyles((theme: Theme) => { 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', @@ -55,8 +65,21 @@ export default function StatusGas(props: Props) { const { device, gasType } = props; const classes = useStyles() // console.log(noDust) - const colors = or(device.status.o2?.overlays.map(overlay => overlay.color), []); - const messages = or(device.status.o2?.overlays.map(overlay => overlay.message), []); + 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) @@ -85,8 +108,8 @@ export default function StatusGas(props: Props) { } const interval = setInterval(() => { if (gasType === "all") setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); - setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); - }, 5000); + else setCurrentIndex((prevIndex) => (prevIndex + 1) % 4); + }, 6000); return () => clearInterval(interval); // Cleanup on unmount }, [gasType]); @@ -105,19 +128,24 @@ export default function StatusGas(props: Props) { {messages.map((message, index) => { return ( - - - - {message} - + + + + + + + {message} + + ) })} @@ -130,8 +158,13 @@ export default function StatusGas(props: Props) { // if (gasType !== "all") { return ( - + + { gasType === "all" && + + {gas.toUpperCase()} + + } {gas !== "o2" ?