diff --git a/src/cableEstimator/cableEstimator.tsx b/src/cableEstimator/cableEstimator.tsx index f587efd..960e06a 100644 --- a/src/cableEstimator/cableEstimator.tsx +++ b/src/cableEstimator/cableEstimator.tsx @@ -61,6 +61,8 @@ export default function CableEstimator() { const [tablePage, setTablePage] = useState(0) const [pageSize, setPageSize] = useState(10) const [rowCount, setRowCount] = useState(0) + const [orderBy, setOrderBy] = useState("") + const [order, setOrder] = useState<"asc" | "desc">("asc") const [selectedBin, setSelectedBin] = useState(); const [quoteOpen, setQuoteOpen] = useState(false); const [useCustomBin, setUseCustomBin] = useState(false); @@ -106,47 +108,57 @@ export default function CableEstimator() { const [loading, setLoading] = useState(false) const columns: Column[] = [ - { - title: "Manufacturer", - render: (row: jsonBin) => {row.Manufacturer} - }, - { - title: "Model", - render: (row: jsonBin) => {row.Model} - }, - { - title: "Capacity", - render: (row: jsonBin) => {row.Capacity} - }, - { - title: "Hopper Angle", - render: (row: jsonBin) => {row.HopperAngle} - }, - { - title: "Diameter " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), - render: (row: jsonBin) => {distanceConversion(row.Diameter).toFixed(2)} - }, - { - title: "Sidewall" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), - render: (row: jsonBin) => {distanceConversion(row.Sidewall).toFixed(2)} - }, - { - title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), - render: (row: jsonBin) => {distanceConversion(row.Peak).toFixed(2)} - }, - { - title: "Eave To Peak" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), - render: (row: jsonBin) => {distanceConversion(row.EaveToPeak).toFixed(2)} - }, - { - title: "Roof Angle", - render: (row: jsonBin) => {row.RoofAngle} - }, - { - title: "Rings", - render: (row: jsonBin) => {row.Rings} - } - ] + { + title: "Manufacturer", + sortKey: "Manufacturer", + render: (row: jsonBin) => {row.Manufacturer} + }, + { + title: "Model", + sortKey: "Model", + render: (row: jsonBin) => {row.Model} + }, + { + title: "Capacity", + sortKey: "Capacity", + render: (row: jsonBin) => {row.Capacity} + }, + { + title: "Hopper Angle", + sortKey: "HopperAngle", + render: (row: jsonBin) => {row.HopperAngle} + }, + { + title: "Diameter " + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + sortKey: "Diameter", + render: (row: jsonBin) => {distanceConversion(row.Diameter).toFixed(2)} + }, + { + title: "Sidewall" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + sortKey: "Sidewall", + render: (row: jsonBin) => {distanceConversion(row.Sidewall).toFixed(2)} + }, + { + title: "Peak Height" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + sortKey: "PeakHeight", + render: (row: jsonBin) => {distanceConversion(row.Peak).toFixed(2)} + }, + { + title: "Eave To Peak" + (getDistanceUnit() === pond.DistanceUnit.DISTANCE_UNIT_FEET ? "(ft)" : "(m)"), + sortKey: "EaveToPeak", + render: (row: jsonBin) => {distanceConversion(row.EaveToPeak).toFixed(2)} + }, + { + title: "Roof Angle", + sortKey: "RoofAngle", + render: (row: jsonBin) => {row.RoofAngle} + }, + { + title: "Rings", + sortKey: "Rings", + render: (row: jsonBin) => {row.Rings} + } + ] useEffect(()=>{ setLoading(true) @@ -154,6 +166,21 @@ export default function CableEstimator() { let clone: jsonBin[] = cloneDeep(binOptions) //having the search filter in here is quite inefficient as it will filter each time you change pages as well //TODO: re-work the filter so it only filters when the search is changed + + // sort the array according to orderBy + clone.sort((a, b) => { + if (Object.keys(a).includes(orderBy)) { + let by = orderBy as keyof jsonBin + if (typeof(a[by]) === "string") { + // return a.localeCompare(b) + return (a[by].localeCompare(b[by] as string)) + } + return ((b[by] as any) - (a[by] as any)) + } + return 0 + }) + if (order === "desc") clone.reverse() + if(tableSearch !== ""){ //filter the data in the cloned list clone = clone.filter(bin => { @@ -175,7 +202,7 @@ export default function CableEstimator() { let rows = clone.splice(tablePage * pageSize, pageSize) setDisplayedRows(rows) setLoading(false) - },[binOptions, tablePage, pageSize, tableSearch]) + },[binOptions, tablePage, pageSize, tableSearch, orderBy, order]) const binOpTable = () => { return ( @@ -193,6 +220,10 @@ export default function CableEstimator() { handleRowsPerPageChange={(e) => {setPageSize(e.target.value)}} rows={displayedRows} total={rowCount} + order={order} + setOrder={setOrder} + orderBy={orderBy} + setOrderBy={setOrderBy} /> ); }; diff --git a/src/common/QrCodeGenerator.tsx b/src/common/QrCodeGenerator.tsx index 8d5ddcb..27eaff0 100644 --- a/src/common/QrCodeGenerator.tsx +++ b/src/common/QrCodeGenerator.tsx @@ -1,4 +1,4 @@ -import { Box, Button, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { Box, Button, Checkbox, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material"; // import MaterialTable from "material-table"; import React, { useEffect, useState } from "react"; import ResponsiveDialog from "./ResponsiveDialog"; @@ -36,7 +36,7 @@ export default function(props: Props) { width: 500 }, viewerMobile: { - height: "100%", + height: 500, width: "100%" }, page: { @@ -103,12 +103,12 @@ export default function(props: Props) { selected.push(row) }else{ //remove the page/index from the map if(pageSelect){ - pageSelect.splice(pageSelect.indexOf(index)) + pageSelect.splice(pageSelect.indexOf(index), 1) } //remove the selected object from the array selected.forEach((codeKey, index) => { if(row.key === codeKey.key){ - selected.splice(index) + selected.splice(index, 1) } }) } @@ -137,35 +137,27 @@ export default function(props: Props) { total={keyList.length} setPage={(page) => {setTablePage(page)}} handleRowsPerPageChange={handleRowsPerPageChange} + loadMore={() => { + let clone: QrCodeKey[] = cloneDeep(keyList) + let current = cloneDeep(displayedRows) + let newRows = clone.splice(current.length, pageSize) + setDisplayedRows(current.concat(newRows)) + }} + renderMobile={(row, index) => { + return ( + + { + rowSelected(row, index, checked) + }} + /> + + {row.name} + + + ) + }} /> - {/* { - setSelectedKeys(data); - }} - columns={[ - { - cellStyle: { - padding: 0 - }, - title: "Name", - field: "name" - } - ]}> */} ); }; diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index de23f1b..ee1e9dd 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -1,6 +1,6 @@ -import { Box, Card, Checkbox, CircularProgress, Collapse, darken, Divider, Grid2, IconButton, InputAdornment, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material"; +import { Box, Button, Card, Checkbox, CircularProgress, Collapse, darken, Divider, Grid2, IconButton, InputAdornment, ListItemButton, ListItemIcon, ListItemText, Menu, MenuItem, Paper, SxProps, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow, TextField, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import { ChevronRight, Close, Search, ViewColumn } from "@mui/icons-material" +import { ArrowDownward, ArrowUpward, ChevronRight, Close, Search, ViewColumn } from "@mui/icons-material" import { useEffect, useState } from "react"; import React from "react"; import { useMobile } from "hooks"; @@ -64,6 +64,8 @@ export interface Column { cellStyle?: SxProps; hidden?: boolean; render: (row: T) => JSX.Element; + sortKey?: string; + disableSort?: boolean; } interface Props { @@ -83,7 +85,7 @@ interface Props { multiGutter?: boolean; rowsPerPageOptions?: number[]; noDataMessage?: string; - renderMobile?: (row: T) => JSX.Element; + renderMobile?: (row: T, index: number) => JSX.Element; hideKeys?: boolean; onRowClick?: (row: T) => void; actions?: string | JSX.Element; @@ -92,6 +94,12 @@ 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 + orderBy?: string; + setOrderBy?: React.Dispatch>; + order?: string; + setOrder?: React.Dispatch>; + endTitleElement?: string | JSX.Element | (() => string | JSX.Element); + loadMore?: () => void } export default function ResponsiveTable(props: Props) { @@ -120,12 +128,19 @@ export default function ResponsiveTable(props: Props) { selectedRows, customElement, mobileView, - hidePagination + hidePagination, + orderBy, + setOrderBy, + order, + setOrder, + loadMore } = props const classes = useStyles(); 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 order = props.order ? props.order : "asc" + const endTitleElement = typeof(props.endTitleElement) === "function" ? props.endTitleElement() : props.endTitleElement const isMobile = useMobile() @@ -258,9 +273,9 @@ export default function ResponsiveTable(props: Props) { return String(val).charAt(0).toUpperCase() + String(val).slice(1); } - const renderMobileRow = (row: T) => { + const renderMobileRow = (row: T, index: number) => { if (renderMobile) return ( - renderMobile(row) + renderMobile(row, index) ) if (columns) { return ( @@ -315,7 +330,7 @@ export default function ResponsiveTable(props: Props) { {rows.map((row, index) => { return ( onRowClick&&onRowClick(row)}> - {renderMobileRow(row)} + {renderMobileRow(row, index)} {renderGutter && <> @@ -360,6 +375,17 @@ export default function ResponsiveTable(props: Props) { ) } + {(loadMore && rows.length < total) && + + + + } ) @@ -373,12 +399,42 @@ export default function ResponsiveTable(props: Props) { setFilterList(newFilterList) } + const columnClick = (column: Column) => { + const sortKey = column.sortKey ? column.sortKey : column.title.toLowerCase() + // console.log(column.sortKey) + if (setOrderBy) { + if (orderBy === sortKey && setOrder && order) { + if (order === "desc") { + setOrder("asc") + } else { + setOrderBy("") + } + // setOrder(order === "asc" ? "desc" : "asc") + } else { + if (setOrder) setOrder("desc") + setOrderBy(sortKey) + } + } + // if (setOrder && order) setOrder(order === "asc" ? "desc" : "asc") + } + + const showOrderIcon = (column: Column) => { + if (!orderBy) return + const sortKey = column.sortKey ? column.sortKey : column.title.toLowerCase() + if (sortKey === orderBy) { + if (order === "asc") { + return + } else { + return + } + } + return null; + } return ( - + - @@ -393,6 +449,9 @@ export default function ResponsiveTable(props: Props) { {actions && actions} + + {endTitleElement} + {setSearchText && {searchBar()} @@ -418,7 +477,6 @@ export default function ResponsiveTable(props: Props) { } - { rowSelect && } @@ -427,8 +485,14 @@ export default function ResponsiveTable(props: Props) { columns.map((column, index) => { if (filterList.includes(column.title) || column.hidden) return null return ( - - {column.title} + !column.disableSort && columnClick(column)} + sx={{ cursor: column.disableSort ? "default" : "pointer" }} + > + + {column.title} {showOrderIcon(column)} + ) }) 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/StatusGas.tsx b/src/common/StatusGas.tsx new file mode 100644 index 0000000..7b40e60 --- /dev/null +++ b/src/common/StatusGas.tsx @@ -0,0 +1,198 @@ +import { Box, Grid2, Theme, Tooltip, Typography } from "@mui/material"; +import { blue, deepOrange, 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), + }, + 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.5), + }, + 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 ( + + + + Overlay Messages + + + {messages.map((message, index) => { + return ( + + + + + + + {message} + + + + ) + })} + + ) + } + + const gasDisplay = () => { + const gas = gasType === "all" ? gasTypes[currentIndex] : gasType + // if (gasType !== "all") { + return ( + + + + { gasType === "all" && + + {gas.toUpperCase()} + + } + + + {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 ( + <> + {gasDisplay()} + + ) +} \ No newline at end of file diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 912f128..288eb10 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', @@ -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) { > - {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 58c131d..cd1f2c6 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/contract/contractTransactionTable.tsx b/src/contract/contractTransactionTable.tsx index 4508377..ebee9ba 100644 --- a/src/contract/contractTransactionTable.tsx +++ b/src/contract/contractTransactionTable.tsx @@ -369,6 +369,12 @@ export default function ContractTransactionTable(props: Props) { total={tableData.length} handleRowsPerPageChange={(e)=>{setPageSize(e.target.value)}} setPage={(page)=>{setTablePage(page)}} + loadMore={()=>{ + let clone: Transaction[] = cloneDeep(tableData) + let current = cloneDeep(displayData) + let newRows = clone.splice(current.length, pageSize) + setDisplayData(current.concat(newRows)) + }} /> {/* (pond.DeviceStatistics.create()) @@ -205,7 +205,7 @@ export default function DevicesSummary(props: Props) { } /> diff --git a/src/device/UpgradeDevice.tsx b/src/device/UpgradeDevice.tsx index e61ee80..8715d23 100644 --- a/src/device/UpgradeDevice.tsx +++ b/src/device/UpgradeDevice.tsx @@ -17,7 +17,7 @@ import { // import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import UpdateIcon from "@mui/icons-material/Update"; import { useDeviceAPI, useFirmwareAPI, usePrevious, useSnackbar } from "hooks"; -import { Device, latestFirmwareVersion, Upgrade } from "models"; +import { Device, Firmware, latestFirmwareVersion, Upgrade } from "models"; import moment from "moment"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { useGlobalState } from "providers"; @@ -50,14 +50,16 @@ export default function UpgradeDevice(props: Props) { const deviceAPI = useDeviceAPI(); const { success, error } = useSnackbar(); const firmwareAPI = useFirmwareAPI(); - const [{ firmware }] = useGlobalState(); - const prevFirmware = usePrevious(firmware); + // const [{ firmware }] = useGlobalState(); const { device } = props; const prevDevice = usePrevious(device); const [open, setOpen] = useState(false); const prevOpen = usePrevious(open); const [upgrade, setUpgrade] = useState(Upgrade.create()); const [gettingStatus, setGettingStatus] = useState(0); + const [firmware, setFirmware] = useState>(new Map()) + const [firmwareLoading, setFirmwareLoading] = useState(false) + const prevFirmware = usePrevious(firmware); const progress = useCallback(() => { return upgrade.status.progress; @@ -75,11 +77,32 @@ export default function UpgradeDevice(props: Props) { ); }; + useEffect(() => { + if (firmwareLoading) return + // if (firmware.size > 0) return + setFirmwareLoading(true) + firmwareAPI.getAllLatestFirmware().then(resp => { + let versions: Map = new Map(); + if (resp && resp.data && resp.data.firmware) { + resp.data.firmware.forEach((raw: any) => { + let firmware = Firmware.any(raw); + let key = firmware.settings.platform + ":" + firmware.settings.channel; + versions.set(key, firmware); + }); + } + // dispatch({ key: "firmware", value: versions }); + setFirmware(versions) + }).finally(() => { + setFirmwareLoading(false) + }) + }, []) + const upgradeVersion = () => { return upgrade.settings.version; }; const available = () => { + if (firmware.size < 1) return false return currentFirmware() !== latestFirmware(); }; diff --git a/src/device/deviceSVG.tsx b/src/device/deviceSVG.tsx index d6399f7..e279268 100644 --- a/src/device/deviceSVG.tsx +++ b/src/device/deviceSVG.tsx @@ -5204,6 +5204,933 @@ export default function DeviceSVG(props: Props) { ); }; + const streamlineMonitorSVG = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* buttons for port selection */} + {/* I2C PORTS */} + { + openMenu(e, { + address: 0, //address here is not important for I2C, the restrictions are per component not per port + addressType: quack.AddressType.ADDRESS_TYPE_I2C, + label: "1", + autoDetectable: false + }); + }} + className={classes.testButton} + id="button" + cx="20.5" + cy="44" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* CONFIGURABLE PIN PORTS - Note input Ports 3 and 4 and all control ports are locked for the monitor */} + {/* PORT 1 */} + { + openMenu(e, { + address: 1, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "1", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="20" + cy="80" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* PORT 2 */} + { + openMenu(e, { + address: 2, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "2", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="20.5" + cy="116" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* PORT 3 */} + { + setLockOpen(true); + // openMenu(e, { + // address: 4, + // addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + // label: "3", + // autoDetectable: true + // }); + }} + className={classes.testButton} + id="button" + cx="21.5" + cy="153" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* Port 4 */} + { + setLockOpen(true); + // openMenu(e, { + // address: 8, + // addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + // label: "3", + // autoDetectable: true + // }); + }} + className={classes.testButton} + id="button" + cx="21.5" + cy="189" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* Controller ports - note these are all locked for monitor devices */} + {/* PORT C1 */} + { + setLockOpen(true); + // openMenu(e, { + // address: 2, + // addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + // label: "2", + // autoDetectable: true + // }); + }} + className={classes.testButton} + id="button" + cx="98" + cy="206" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* PORT C2 */} + { + setLockOpen(true); + // openMenu(e, { + // address: 4, + // addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + // label: "3", + // autoDetectable: true + // }); + }} + className={classes.testButton} + id="button" + cx="140" + cy="206" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* Port C3 */} + { + setLockOpen(true); + // openMenu(e, { + // address: 8, + // addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + // label: "3", + // autoDetectable: true + // }); + }} + className={classes.testButton} + id="button" + cx="182" + cy="206" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + + ); + }; + + const streamlineAutomateSVG = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* buttons for port selection */} + {/* I2C PORTS */} + { + openMenu(e, { + address: 0, //address here is not important for I2C, the restrictions are per component not per port + addressType: quack.AddressType.ADDRESS_TYPE_I2C, + label: "1", + autoDetectable: false + }); + }} + className={classes.testButton} + id="button" + cx="17" + cy="44" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* CONFIGURABLE PIN PORTS - Note input Ports 3 and 4 and all control ports are locked for the monitor */} + {/* PORT 1 */} + { + openMenu(e, { + address: 1, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "1", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="17" + cy="80" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* PORT 2 */} + { + openMenu(e, { + address: 2, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "2", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="17" + cy="116" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* PORT 3 */} + { + openMenu(e, { + address: 4, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "3", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="17" + cy="152" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* Port 4 */} + { + openMenu(e, { + address: 8, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "4", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="17" + cy="188" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* Controller ports - note these are all locked for monitor devices */} + {/* PORT C1 */} + { + openMenu(e, { + address: 2, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "C1", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="95" + cy="207" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* PORT C2 */} + { + openMenu(e, { + address: 4, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "C2", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="138" + cy="207" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + {/* Port C3 */} + { + openMenu(e, { + address: 8, + addressType: quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, + label: "C3", + autoDetectable: true + }); + }} + className={classes.testButton} + id="button" + cx="180" + cy="207" + rx={buttonSize.rxLarge} + ry={buttonSize.ryLarge} + /> + + ); + }; + const getProductSVG = () => { switch (device.settings.product) { case pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO: @@ -5232,6 +6159,10 @@ export default function DeviceSVG(props: Props) { return miVentSVG(); case pond.DeviceProduct.DEVICE_PRODUCT_MIVENT_V2: return miVentSVG(true); + case pond.DeviceProduct.DEVICE_PRODUCT_STREAMLINE_MONITOR: + return streamlineMonitorSVG(); + case pond.DeviceProduct.DEVICE_PRODUCT_STREAMLINE_AUTOMATE: + return streamlineAutomateSVG(); } }; diff --git a/src/firmware/UploadFirmware.tsx b/src/firmware/UploadFirmware.tsx index d985f7a..aa5aa41 100644 --- a/src/firmware/UploadFirmware.tsx +++ b/src/firmware/UploadFirmware.tsx @@ -79,7 +79,8 @@ class UploadFirmware extends React.Component { pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK, pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN, pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE, - pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE + pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE, + pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE ].includes(platform); var isChannelValid = [ pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA, @@ -229,6 +230,9 @@ class UploadFirmware extends React.Component { V2 Wifi Blue + + + V2 Ethernet Blue 1 && data[data.length - 1].value > gate.lowerFlow() && data[data.length - 1].value < gate.upperFlow() ) { diff --git a/src/gate/GateList.tsx b/src/gate/GateList.tsx index 8f0c8bc..5c548e2 100644 --- a/src/gate/GateList.tsx +++ b/src/gate/GateList.tsx @@ -12,6 +12,7 @@ import AddGateFab from "./AddGateFab"; import GateSettings from "./GateSettings"; import { useGateAPI, useGlobalState } from "providers"; import { Settings } from "@mui/icons-material"; +import { cloneDeep } from "lodash"; interface Props { //gates: Gate[]; @@ -219,6 +220,19 @@ export default function GateList(props: Props) { onRowClick={(gate) => {goToGate(gate)}} hideKeys={isMobile || props.useMobile} setSearchText={(search) => setSearch(search)} + loadMore={()=>{ + let current = cloneDeep(gates) + let keys = undefined + let types = undefined + if(currentTerminal) { + keys = [currentTerminal] + types = ["terminal"] + } + gateAPI.listGates(pageSize, current.length , "asc", "name", search, undefined, keys, types, undefined, undefined, as).then(resp => { + let newGates = resp.data.gates.map(gate => Gate.create(gate)) + setGates(current.concat(newGates)) + }) + }} /> ) }; diff --git a/src/maps/mapDrawers/GroupDrawer.tsx b/src/maps/mapDrawers/GroupDrawer.tsx index 4048245..e9d2768 100644 --- a/src/maps/mapDrawers/GroupDrawer.tsx +++ b/src/maps/mapDrawers/GroupDrawer.tsx @@ -3,6 +3,7 @@ import DisplayDrawer from "common/DisplayDrawer"; import ResponsiveTable from "common/ResponsiveTable"; //import DeviceCardList from "device/DeviceCardList"; import { useGroupAPI, useSnackbar } from "hooks"; +import { cloneDeep } from "lodash"; import { Group } from "models"; import moment from "moment"; import { describePower } from "pbHelpers/Power"; @@ -28,18 +29,19 @@ export default function GroupDrawer(props: Props) { const { openSnack } = useSnackbar(); const [grpDevs, setGrpDevs] = useState([]); const navigate = useNavigate(); - const [tablePage, setTablePage] = useState(0) - const [pageSize, setPageSize] = useState(10) + const loadLimit = 10 + const [groupTotal, setGroupTotal] = useState(0) const loadDevs = useCallback(() => { groupAPI - .listGroupDevices(group.id(), 100, 0, "asc", undefined, true) + .listGroupDevices(group.id(), loadLimit, 0, "asc", undefined, true) .then(resp => { const groupDevices: pond.ComprehensiveDevice[] = or( resp.data.comprehensiveDevices, [] ).map(d => pond.ComprehensiveDevice.fromObject(d)); setGrpDevs(groupDevices); + setGroupTotal(resp.data.total) }) .catch(err => { openSnack("Failed to get devices for group"); @@ -174,21 +176,37 @@ export default function GroupDrawer(props: Props) { } const drawerBody = () => { - // TODO: will need to update the rows and setPage functions if we decide to have the table only show a number of items matching its page size rether than everything passed in return ( - page={tablePage} - pageSize={pageSize} + page={0} + pageSize={loadLimit} onRowClick={toDevice} - handleRowsPerPageChange={()=>{}} - setPage={()=>{}} - total={0} + total={groupTotal} rows={grpDevs} renderMobile={mobile} renderGutter={gutter} gutterPadding={0} mobileView + loadMore={() => { + let current = cloneDeep(grpDevs) + groupAPI + .listGroupDevices(group.id(), loadLimit, current.length, "asc", undefined, true) + .then(resp => { + let newDevices: pond.ComprehensiveDevice[] = or( + resp.data.comprehensiveDevices, + [] + ).map(d => pond.ComprehensiveDevice.fromObject(d)); + setGrpDevs(current.concat(newDevices)); + setGroupTotal(resp.data.total) + }) + .catch(err => { + openSnack("Failed to get devices for group"); + }); + }} + //these two function are uneccessary as this will always be in a list as mobile view + handleRowsPerPageChange={()=>{}} + setPage={()=>{}} /> ) diff --git a/src/models/Device.ts b/src/models/Device.ts index b99510e..b201407 100644 --- a/src/models/Device.ts +++ b/src/models/Device.ts @@ -14,6 +14,7 @@ interface FeatureVersionByPlatform { v2CellGreen: string; v2WifiBlue: string; v2CellBlue: string; + v2EthBlue: string; } const featureVersions: Map = new Map([ @@ -29,7 +30,8 @@ const featureVersions: Map = new Map([ v2CellBlack: "2.0.44", v2CellGreen: "2.0.44", v2WifiBlue: "2.0.44", - v2CellBlue: "2.0.44" + v2CellBlue: "2.0.44", + v2EthBlue: "2.0.44" } ] ]); @@ -118,6 +120,8 @@ export class Device { return this.status.firmwareVersion >= versions.v2WifiBlue; case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE: return this.status.firmwareVersion >= versions.v2CellBlue; + case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE: + return this.status.firmwareVersion >= versions.v2EthBlue; default: return false; } diff --git a/src/models/Firmware.ts b/src/models/Firmware.ts index 99e09dd..564b396 100644 --- a/src/models/Firmware.ts +++ b/src/models/Firmware.ts @@ -8,6 +8,8 @@ export function latestFirmwareVersion( channel: pond.UpgradeChannel ): string { const firmware = versions.get(platform + ":" + channel); + // console.log(platform + ":" + channel) + // console.log(versions) if (firmware) { return firmware.settings.version; } diff --git a/src/objectHeater/ObjectHeaterCard.tsx b/src/objectHeater/ObjectHeaterCard.tsx index 4255b1f..651ae7c 100644 --- a/src/objectHeater/ObjectHeaterCard.tsx +++ b/src/objectHeater/ObjectHeaterCard.tsx @@ -371,7 +371,7 @@ export default function ObjectHeaterCard(props: Props) { }else { return ( - + { setOpenQrGenerator(true); + setBinMenuAnchorEl(null) }}> Generate QR Codes 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/DeviceComponent.tsx b/src/pages/DeviceComponent.tsx index 424de58..c04a1b2 100644 --- a/src/pages/DeviceComponent.tsx +++ b/src/pages/DeviceComponent.tsx @@ -23,6 +23,7 @@ import { } from "hooks"; import InteractionChip from "interactions/InteractionChip"; import InteractionSettings from "interactions/InteractionSettings"; +import { cloneDeep } from "lodash"; // import MaterialTable from "material-table"; import { Component, Device, deviceScope, Group, Interaction } from "models"; import { convertedUnitMeasurement, MeasurementsFor, UnitMeasurement } from "models/UnitMeasurement"; @@ -450,7 +451,6 @@ export default function DeviceComponent() { // }); }) .catch((err: any) => { - // resolve({ // data: [], // page: 0, @@ -613,6 +613,32 @@ export default function DeviceComponent() { pageSize={pageSize} setPage={setPage} handleRowsPerPageChange={handleRowsPerPageChange} + loadMore={() => { + let currentRows = cloneDeep(rows) + let newOffset = rows.length + componentAPI.listUnitMeasurements( + deviceID, + componentID, + startDate, + endDate, + pageSize, + newOffset, + order, + undefined, + getContextKeys(), + getContextTypes(), + showErrors, + as) + .then(resp => { + let newRows = UnitMeasurement.convertMeasurements( + resp.data.measurements.map(um => UnitMeasurement.any(um, user)) + ) + setRows(currentRows.concat(newRows)) + }).catch(err => { + + }) + + }} /> ); diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index 20bf476..a6a19de 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -301,6 +301,9 @@ export default function DeviceSupport() { V2 Cellular Blue + + V2 Ethernet Blue + + } ); }; diff --git a/src/pbHelpers/ComponentTypes/DragerGasDongle.ts b/src/pbHelpers/ComponentTypes/DragerGasDongle.ts index c605718..a8f2090 100644 --- a/src/pbHelpers/ComponentTypes/DragerGasDongle.ts +++ b/src/pbHelpers/ComponentTypes/DragerGasDongle.ts @@ -87,6 +87,11 @@ export function dragerGasDongle(subtype: number = 0): ComponentTypeExtension { key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_H2S, value: "DRAGER_GAS_DONGLE_SUBTYPE_H2S", friendlyName: "Drager Gas H2S" + }, + { + key: quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_LEL, + value: "DRAGER_GAS_DONGLE_SUBTYPE_LEL", + friendlyName: "Drager Gas LEL" } ], friendlyName: "Drager Gas", @@ -162,7 +167,8 @@ export function dragerGasDongle(subtype: number = 0): ComponentTypeExtension { lines: [], average: [] }; - if (subtype === quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_O2) { + //sets the value as a percent for the chart data + if (subtype === quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_O2 || subtype === quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_LEL) { let firstTime: Moment | undefined; let lastTime: Moment | undefined; let valMap: Map = new Map(); diff --git a/src/pbHelpers/ComponentTypes/Pressure.ts b/src/pbHelpers/ComponentTypes/Pressure.ts index 3da52c4..c246f69 100644 --- a/src/pbHelpers/ComponentTypes/Pressure.ts +++ b/src/pbHelpers/ComponentTypes/Pressure.ts @@ -45,6 +45,11 @@ export function Pressure(subtype: number = 0): ComponentTypeExtension { key: quack.PressureSubtype.PRESSURE_SUBTYPE_FROG, value: "PRESSURE_SUBTYPE_FROG", friendlyName: "Pressure (Frog)" + } as Subtype, + { + key: quack.PressureSubtype.PRESSURE_SUBTYPE_DIFFERENTIAL_PRESSURE, + value: "PRESSURE_SUBTYPE_DIFFERENTIAL_PRESSURE", + friendlyName: "Pressure Differential" } as Subtype ], friendlyName: "Pressure", diff --git a/src/pbHelpers/Connectivity.tsx b/src/pbHelpers/Connectivity.tsx index 3a6795d..17da3bc 100644 --- a/src/pbHelpers/Connectivity.tsx +++ b/src/pbHelpers/Connectivity.tsx @@ -73,6 +73,8 @@ function getTooltip(platform: pond.DevicePlatform): string { case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON || pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3: return "Communicates via Wi-Fi"; + case pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE: + return "Communicates view Ethernet" default: return ""; } diff --git a/src/pbHelpers/DevicePlatform.tsx b/src/pbHelpers/DevicePlatform.tsx index 319f82b..6090265 100644 --- a/src/pbHelpers/DevicePlatform.tsx +++ b/src/pbHelpers/DevicePlatform.tsx @@ -4,7 +4,9 @@ import { SignalCellular4Bar, SignalWifi4Bar, SignalCellularOff, - SignalWifiOff + SignalWifiOff, + Cable, + DoNotDisturb } from "@mui/icons-material"; import { pond } from "protobuf-ts/pond"; @@ -98,6 +100,15 @@ const V2_Wifi_Blue: DevicePlatformDescriber = { platformName: "v2wifiblue" }; +const V2_Ethernet_Blue: DevicePlatformDescriber = { + platform: pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE, + label: "V2 Ethernet Blue", + description: "Communicates via ethernet", + icon: , + offlineIcon: , + platformName: "v2ethernetblue" +}; + const map = new Map([ [pond.DevicePlatform.DEVICE_PLATFORM_INVALID, Default], [pond.DevicePlatform.DEVICE_PLATFORM_PHOTON, Photon], @@ -107,7 +118,8 @@ const map = new Map([ [pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK, V2_Cell_Black], [pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN, V2_Cell_Green], [pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE, V2_Cell_Blue], - [pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE, V2_Wifi_Blue] + [pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE, V2_Wifi_Blue], + [pond.DevicePlatform.DEVICE_PLATFORM_V2_ETHERNET_BLUE, V2_Ethernet_Blue] ]); export function getDevicePlatformDescriber(platform: pond.DevicePlatform): DevicePlatformDescriber { diff --git a/src/pbHelpers/MeasurementDescriber.ts b/src/pbHelpers/MeasurementDescriber.ts index ee45c8f..a364b77 100644 --- a/src/pbHelpers/MeasurementDescriber.ts +++ b/src/pbHelpers/MeasurementDescriber.ts @@ -334,6 +334,7 @@ export class MeasurementDescriber { this.details.unit = "mV"; break; case quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_O2: + case quack.DragerGasDongleSubtype.DRAGER_GAS_DONGLE_SUBTYPE_LEL: this.details.colour = blue["400"]; this.details.decimals = 1; this.details.unit = "%"; @@ -342,7 +343,7 @@ export class MeasurementDescriber { mode: "toStored" | "toDisplay" ): number => { if (mode === "toStored") { - //for O2 dragers the display is in percentage so convert the percent to ppm + //for O2/LEL dragers the display is in percentage so convert the percent to ppm return value * 10000; } return value / 10000; diff --git a/src/products/Bindapt/BindaptDescriber.ts b/src/products/Bindapt/BindaptDescriber.ts index 2bb4b63..b7f39f3 100644 --- a/src/products/Bindapt/BindaptDescriber.ts +++ b/src/products/Bindapt/BindaptDescriber.ts @@ -191,6 +191,38 @@ export const BindaptV2Automate: DeviceProductDescriber = { // ]) }; +export const StreamlineMonitor: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_STREAMLINE_MONITOR, + label: "Streamline Monitor", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptV2MonitorAvailability + //TODO not sure what the default address map for these should be yet + // defaultAddressMap: new Map([ + // ["plenum", ["9-2-64"]], + // ["heatersFans", ["3-1-512"]] //refers to heaters and fans + // ]) +}; + +export const StreamlineAutomate: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_STREAMLINE_AUTOMATE, + label: "Streamline Automate", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptV2AutomateAvailability + //TODO not sure what the default address map for these should be yet + // defaultAddressMap: new Map([ + // ["plenum", ["9-2-64"]], + // ["heatersFans", ["3-1-512"]] //refers to heaters and fans + // ]) +}; + export function IsBindaptDevice(product?: pond.DeviceProduct): boolean { const bindaptProducts: pond.DeviceProduct[] = [ pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI, diff --git a/src/products/DeviceProduct.ts b/src/products/DeviceProduct.ts index ce910ec..792be79 100644 --- a/src/products/DeviceProduct.ts +++ b/src/products/DeviceProduct.ts @@ -11,7 +11,9 @@ import { BindaptV2Automate, BinHalo, BinMonitor, - BinUltimate + BinUltimate, + StreamlineMonitor, + StreamlineAutomate } from "./Bindapt/BindaptDescriber"; import { NexusST } from "./Nexus/NexusDescriber"; import { MiPCAV2, OmniAir } from "./OmniAir/OmniAirDescriber"; @@ -19,6 +21,7 @@ import { ConfigurablePin } from "pbHelpers/AddressTypes"; import { useTheme } from "@mui/material"; import { Device } from "models"; import { or } from "utils"; +import { MiVent, MiVentV2 } from "./MiVent/MiVentDescriber"; export type ProductTab = "components" | "presets"; @@ -87,7 +90,11 @@ const DEVICE_PRODUCT_MAP = new Map([ [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD, BindaptPlusMod], [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD, BindaptPlusProMod], [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR, BindaptV2Monitor], - [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE, BindaptV2Automate] + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE, BindaptV2Automate], + [pond.DeviceProduct.DEVICE_PRODUCT_MIVENT, MiVent], + [pond.DeviceProduct.DEVICE_PRODUCT_MIVENT_V2, MiVentV2], + [pond.DeviceProduct.DEVICE_PRODUCT_STREAMLINE_MONITOR, StreamlineMonitor], + [pond.DeviceProduct.DEVICE_PRODUCT_STREAMLINE_AUTOMATE, StreamlineAutomate] ]); export function ListDeviceProductDescribers(): DeviceProductDescriber[] { diff --git a/src/products/MiVent/MiVentAvailability.ts b/src/products/MiVent/MiVentAvailability.ts new file mode 100644 index 0000000..4002627 --- /dev/null +++ b/src/products/MiVent/MiVentAvailability.ts @@ -0,0 +1,61 @@ +import { ConfigurablePin } from "pbHelpers/AddressTypes"; +import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability"; +import { quack } from "protobuf-ts/pond"; + +const MiVentV1Pins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 512, label: "C1" }, + { address: 1024, label: "C2" } +]; + +export const MiVentV1Availability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, MiVentV1Pins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], + [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x2a]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +/*---V2 Stuff starts here---*/ + +const MiVentV2Pins: ConfigurablePin[] = [ + { address: 1, label: "1" }, + { address: 2, label: "2" }, + { address: 4, label: "3" }, + { address: 256, label: "L1" }, + { address: 512, label: "L2" } +]; + +export const MiVentV2Availability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, MiVentV2Pins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]], + [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x2a]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); diff --git a/src/products/MiVent/MiVentDescriber.ts b/src/products/MiVent/MiVentDescriber.ts new file mode 100644 index 0000000..05592ec --- /dev/null +++ b/src/products/MiVent/MiVentDescriber.ts @@ -0,0 +1,29 @@ +import { DeviceProductDescriber } from "products/DeviceProduct"; +import { pond } from "protobuf-ts/pond"; +import DeviceDarkIcon from "assets/products/Ag/device 1.png"; +import DeviceLightIcon from "assets/products/Ag/device 2.png"; +import { MiVentV1Availability, MiVentV2Availability } from "./MiVentAvailability"; + +export const MiVent: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_MIVENT, + label: "MiVent V1", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? DeviceDarkIcon : DeviceLightIcon; + }, + view: "controller", + tabs: ["components"], + availability: MiVentV1Availability +}; + +/*---V2 Stuff starts here---*/ + +export const MiVentV2: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_MIVENT_V2, + label: "MiVent V2", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? DeviceDarkIcon : DeviceLightIcon; + }, + view: "controller", + tabs: ["components"], + availability: MiVentV2Availability +}; 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}); } diff --git a/src/teams/TeamList.tsx b/src/teams/TeamList.tsx index 6bb313f..7742033 100644 --- a/src/teams/TeamList.tsx +++ b/src/teams/TeamList.tsx @@ -251,6 +251,47 @@ export default function TeamList() { isLoading={loading} renderGutter={isMobile ? renderGutter : undefined} hideKeys + loadMore={() => { + let current = cloneDeep(teams) + setLoading(true); + let newTeamPerms: Map = new Map(); + let newTeamPrefs: Map = new Map(); + let viewAs: string | undefined = as + if (as === team.key()) { + viewAs = undefined + } + teamAPI + .listTeams(limit, current.length, undefined, "name", undefined, undefined, " ", search) + .then(resp => { + setTotal(resp.data.total ? resp.data.total : 0); + let newTeams: Team[] = [] + resp.data.teams.forEach(team => { + let u = viewAs ? viewAs : user.id(); + let t = Team.create(team) + if (team.settings && u !== "") { + userAPI + .getUser(u, teamScope(team.settings.key)) + .then(resp => { + newTeamPerms.set(t.key(), resp.permissions) + newTeamPrefs.set(t.key(), resp.preferences) + t.preferences = cloneDeep(resp.preferences) + t.permissions = cloneDeep(resp.permissions) + }).catch(() => { + snackbar.error("Error loading teams.") + }) + .finally(() => { + setTeamPerms(newTeamPerms); + setTeamPrefs(newTeamPrefs); + }); + } + newTeams.push(t) + }); + setTeams(current.concat(newTeams)); + }) + .finally(() => { + setLoading(false); + }); + }} />