diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 4c3f5a3..b85a237 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -16,6 +16,7 @@ import ConstructionSiteMap from "pages/ConstructionSiteMap"; import Sites from "pages/Sites"; import Site from "pages/Site"; import Heaters from "pages/Heaters"; +import Heater from "pages/Heater"; //import Site from "pages/Site"; const DeviceHistory = lazy(() => import("pages/DeviceHistory")); @@ -215,14 +216,14 @@ export default function Router(props: Props) { path="" // "/settings/basic" element={} /> - {/* } + element={} /> } - /> */} + /> ); diff --git a/src/objectHeater/ObjectHeaterActions.tsx b/src/objectHeater/ObjectHeaterActions.tsx new file mode 100644 index 0000000..06357ab --- /dev/null +++ b/src/objectHeater/ObjectHeaterActions.tsx @@ -0,0 +1,210 @@ +import { + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Tooltip +} from "@mui/material"; +import SettingsIcon from "@mui/icons-material/Settings"; +import MoreIcon from "@mui/icons-material/MoreVert"; +import React, { useState } from "react"; +import ObjectHeaterSettings from "./ObjectHeaterSettings"; +import { ObjectHeater } from "models/ObjectHeater"; +import ObjectUsersIcon from "@mui/icons-material/AccountCircle"; +import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle"; +import ObjectUsers from "user/ObjectUsers"; +import { pond } from "protobuf-ts/pond"; +import { Component, Scope } from "models"; +import ObjectTeams from "teams/ObjectTeams"; +import RemoveSelfFromObject from "user/RemoveSelfFromObject"; +import ShareObject from "user/ShareObject"; +import { blue } from "@mui/material/colors"; +import RemoveSelfIcon from "@mui/icons-material/ExitToApp"; +import { Share } from "@mui/icons-material"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles(() => ({ + shareIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + removeIcon: { + color: "var(--status-alert)" + }, + red: { + color: "var(--status-alert)" + }, + blueIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + } + }) +); +interface OpenState { + users: boolean; + teams: boolean; + settings: boolean; + removeSelf: boolean; + share: boolean; +} + +interface Props { + heater: ObjectHeater; + componentsByDevice?: Map; + refreshCallback: () => void; + permissions: pond.Permission[]; + immediateUpdate: (updatedHeater: ObjectHeater) => void; +} + +export default function ObjectHeaterActions(props: Props) { + const classes = useStyles(); + const { heater, refreshCallback, permissions, componentsByDevice, immediateUpdate } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [openState, setOpenState] = useState({ + users: false, + teams: false, + settings: false, + removeSelf: false, + share: false + }); + + const groupMenu = () => { + return ( + setAnchorEl(null)} + keepMounted + disableAutoFocusItem> + {permissions.includes(pond.Permission.PERMISSION_SHARE) && ( + { + setOpenState({ ...openState, share: true }); + setAnchorEl(null); + }} + dense> + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + { + setOpenState({ ...openState, users: true }); + setAnchorEl(null); + }}> + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + { + setOpenState({ ...openState, teams: true }); + setAnchorEl(null); + }}> + + + + + + )} + { + setOpenState({ ...openState, removeSelf: true }); + setAnchorEl(null); + }}> + + + + + + + ); + }; + + const dialogs = () => { + const key = heater.key; + const label = heater.name; + return ( + + { + if (newHeater) { + immediateUpdate(newHeater); + } + setOpenState({ ...openState, settings: false }); + }} + /> + setOpenState({ ...openState, share: false })} + /> + setOpenState({ ...openState, users: false })} + refreshCallback={refreshCallback} + /> + setOpenState({ ...openState, teams: false })} + refreshCallback={refreshCallback} + /> + { + setOpenState({ ...openState, removeSelf: false }); + }} + /> + + ); + }; + + return ( + + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + setOpenState({ ...openState, settings: true })}> + + + + )} + ) => setAnchorEl(event.currentTarget)}> + + + {dialogs()} + {groupMenu()} + + ); +} diff --git a/src/objectHeater/ObjectHeaterCharts.tsx b/src/objectHeater/ObjectHeaterCharts.tsx new file mode 100644 index 0000000..8a93de6 --- /dev/null +++ b/src/objectHeater/ObjectHeaterCharts.tsx @@ -0,0 +1,621 @@ +import { + Avatar, + Box, + Card, + CardContent, + CardHeader, + Grid2 as Grid, + Theme, + Typography, + useTheme +} from "@mui/material"; +import { useDeviceAPI, useGlobalState, useMutationAPI } from "providers"; +import { Component, Device } from "models"; +import moment from "moment"; +import { pond } from "protobuf-ts/pond"; +import { quack } from "protobuf-ts/quack"; +import { GetDefaultDateRange } from "common/time/DateRange"; +import React, { useEffect, useState } from "react"; +import MultiLineGraph, { LineData } from "charts/measurementCharts/MultiLineGraph"; +import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; +import DateSelect from "common/time/DateSelect"; +import { + Area, + AreaChart, + ReferenceArea, + ResponsiveContainer, + Tooltip, + TooltipProps, + XAxis, + YAxis +} from "recharts"; +import FuelDark from "assets/components/fuelDark.png"; +import FuelLight from "assets/components/fuelLight.png"; +import { useThemeType } from "hooks"; +import { GetComponentIcon } from "pbHelpers/ComponentType"; +import UnitMeasurementSummary from "component/UnitMeasurementSummary"; +import { UnitMeasurement } from "models/UnitMeasurement"; +import MaterialChartTooltip from "charts/MaterialChartTooltip"; +import { roundTo } from "utils"; +import { makeStyles } from "@mui/styles"; +//import TimeBar from "common/time/TimeBar"; + +interface Props { + device: pond.ComprehensiveDevice; + linearMutations: pond.LinearMutation[]; + xDomain: string[] | number[]; + changeXDomain: (domain: string[] | number[]) => void; + getFuelLevel?: (level: string) => void; +} + +interface GraphData { + time: number; + value: number; +} + +interface MutationData { + mutation: pond.LinearMutation; + data: pond.ValueAt[]; +} + +const useStyles = makeStyles((theme: Theme) => ({ + avatarIcon: { + width: theme.spacing(3), + height: theme.spacing(3) + }, + card: { + height: 550 + }, + cardHeader: { + height: "20%" + }, + cardContent: { + height: "80%" + } + }) +); + +export default function ObjectHeaterCharts(props: Props) { + const { device, linearMutations, getFuelLevel, xDomain, changeXDomain } = props; + const deviceAPI = useDeviceAPI(); + const mutationAPI = useMutationAPI(); + const [tempHum, setTempHum] = useState(); + const [ambientTemps, setAmbientTemps] = useState([]); + const [heaterTemps, setHeaterTemps] = useState([]); + const [heaterState, setHeaterState] = useState([]); + const [tempOnly, setTempOnly] = useState(); + const [heater, setHeater] = useState(); + const [start, setStart] = useState(GetDefaultDateRange().start); + const [end, setEnd] = useState(GetDefaultDateRange().end); + const now = moment(); + const theme = useTheme(); + const [mutationData, setMutationData] = useState([]); + const [{ user }] = useGlobalState(); + const themeType = useThemeType(); + const classes = useStyles(); + const [mutationsLoading, setMutationsLoading] = useState(false); + const [refLeft, setRefLeft] = useState(); + const [refRight, setRefRight] = useState(); + + useEffect(() => { + if (!mutationsLoading) { + setMutationsLoading(true); + //make call to the backend to get measurements for the mutation + let md: MutationData[] = []; + linearMutations.forEach(mut => { + mutationAPI.linearMutation(mut, start.toISOString(), end.toISOString()).then(resp => { + let d: pond.ValueAt[] = resp.data.values; + if (mut.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) { + d = []; + resp.data.values.forEach(valAt => { + d.push( + pond.ValueAt.create({ + time: valAt.time, + value: valAt.value / 10 + }) + ); + }); + } + md.push({ + mutation: mut, + data: d + }); + if (md.length >= linearMutations.length) { + setMutationsLoading(false); + } + setMutationData([...md]); + if (getFuelLevel && mut.mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) { + if (resp.data.values.length > 0) { + getFuelLevel( + (resp.data.values[resp.data.values.length - 1].value / 10).toFixed(2) + "%" + ); + } + } + }); + }); + } + }, [linearMutations, start, end, getFuelLevel, mutationAPI]); //eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + let tempHum: Component | undefined = undefined; + let tempOnly: Component | undefined = undefined; + let heater: Component | undefined = undefined; + + let dev = Device.any(device.device); + let components = device.components; + let measurementsFor: Component[] = []; + components.forEach(comp => { + let c = Component.any(comp); + if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_DHT) { + tempHum = c; + measurementsFor.push(c); + } + if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_TEMPERATURE) { + tempOnly = c; + measurementsFor.push(c); + } + if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT) { + if (c.subType() === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER) { + heater = c; + measurementsFor.push(c); + } + } + }); + setTempHum(tempHum); + setTempOnly(tempOnly); + setHeater(heater); + deviceAPI + .listJSONMeasurements(dev.id(), measurementsFor, start, end, 0, 0, "asc", "timestamp") + .then(resp => { + resp.data.components.forEach(comp => { + if (comp.componentId === tempHum?.key()) { + if (comp.measurementType === "Temperature") { + let lineData: LineData[] = []; + comp.exportedMeasurements.forEach(m => { + lineData.push({ + timestamp: moment(m.timestamp).valueOf(), + points: m.measurementValue.map((t, i) => ({ value: convertTemp(t), node: i })) + }); + }); + setAmbientTemps(lineData); + } + } + if (comp.componentId === tempOnly?.key()) { + if (comp.measurementType === "Temperature") { + setHeaterTemps(comp.exportedMeasurements); + } + } + if (comp.componentId === heater?.key()) { + if (comp.measurementType === "State") { + setHeaterState(comp.exportedMeasurements); + } + } + }); + }); + }, [device, deviceAPI, start, end]); //eslint-disable-line react-hooks/exhaustive-deps + + const updateDateRange = (newStartDate: any, newEndDate: any, live: boolean) => { + setStart(newStartDate); + setEnd(newEndDate); + // setLive(live); + // if (props.updateDate) { + // props.updateDate(newStartDate, newEndDate, live); + // } + }; + + const convertTemp = (temp: number) => { + let t = temp; + if (user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + t = temp * 1.8 + 32; + } + return t; + }; + + //had to make custom chart for this in order to show the heater state and the temp of the ds18 + const heaterStateWithTemps = (tempComponent: Component, heaterComponent: Component) => { + let tempDescriber = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE, + tempComponent.type(), + tempComponent.subType() + ); + let heaterDescriber = describeMeasurement( + quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN, + heaterComponent.type(), + heaterComponent.subType() + ); + let tempData: LineData[] = []; + heaterTemps.forEach(ht => { + tempData.push({ + timestamp: moment(ht.timestamp).valueOf(), + points: ht.measurementValue.map((t, i) => ({ value: convertTemp(t), node: i })) + }); + }); + let stateData: LineData[] = []; + heaterState.forEach(hs => { + stateData.push({ + timestamp: moment(hs.timestamp).valueOf(), + points: hs.measurementValue.map((s, i) => ({ value: s, node: i })) + }); + }); + return ( + + { + changeXDomain(domain); + }} + multiGraphZoomOut + /> + { + changeXDomain(domain); + }} + multiGraphZoomOut + /> + + ); + }; + + //// TODO-CS: Make a mutator describer to put the get functions into //// + const getMutationIcon = (mutation: pond.Mutator, themeType: string) => { + if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) { + return themeType === "light" ? FuelDark : FuelLight; + } + return ""; + }; + + const getMutationLabel = (mutation: pond.Mutator) => { + if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) { + return "Fuel Level"; + } + return ""; + }; + + const getMutationColour = (mutation: pond.Mutator) => { + if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) { + return "lightBlue"; + } + return ""; + }; + + const getMutationUnit = (mutation: pond.Mutator) => { + if (mutation === pond.Mutator.MUTATOR_FUEL_LEVEL) { + return "%"; + } + return ""; + }; + + //// /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ //// + + const zoom = () => { + let newDomain: number[] | string[] = ["dataMin", "dataMax"]; + if (refLeft && refRight && refLeft !== refRight) { + refLeft < refRight ? (newDomain = [refLeft, refRight]) : (newDomain = [refRight, refLeft]); + setRefLeft(undefined); + setRefRight(undefined); + changeXDomain(newDomain); + } + }; + + const mutationGraphCard = (mutation: pond.LinearMutation, data: pond.ValueAt[], key: string) => { + let graphData: GraphData[] = []; + let lastReading: pond.ValueAt | undefined; + data.forEach(d => { + graphData.push({ + time: moment(d.time).valueOf(), + value: d.value + }); + lastReading = d; + }); + return ( + + + {graphData.length > 0 ? ( + + + } + title={mutation.mutationName} + titleTypographyProps={{ variant: "subtitle1" }} + subheader={ + + + + + {getMutationLabel(mutation.mutation) + ": "}{" "} + + {lastReading?.value.toFixed(2) + getMutationUnit(mutation.mutation)} + {" "} +
+
+
+
+ + + {moment(lastReading?.time).fromNow()} + + +
+ } + /> + + + { + if (e) { + setRefLeft(e.activeLabel); + } + }} + onMouseMove={(e: any) => { + if (e) { + setRefRight(e.activeLabel); + } + }} + onMouseUp={() => { + setRefLeft(undefined); + setRefRight(undefined); + zoom(); + }}> + moment(timestamp).format("lll")} + content={(props: TooltipProps) => { + return ( + + roundTo(parseFloat(String(value)), 2).toString() + } + /> + ); + }} + /> + { + let t = moment(timestamp); + return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD"); + }} + scale="time" + type="number" + tick={{ fill: theme.palette.text.primary }} + stroke={theme.palette.divider} + interval="preserveStartEnd" + /> + + + {refLeft && refRight ? ( + + ) : null} + + + +
+ ) : ( + + + No measurements for selected timeframe + + + )} +
+
+ ); + }; + + const mutationGraphs = () => { + let graphCards: JSX.Element[] = []; + if (mutationData.length === 0) { + return ( + + + + No Calculated Measurements Set + + + + ); + } + mutationData.forEach((mutation, i) => { + graphCards.push(mutationGraphCard(mutation.mutation, mutation.data, "mutation" + i)); + }); + return graphCards; + }; + + return ( + + {/* */} + + + {tempOnly && heater && ( + + {heaterTemps.length > 0 ?? heaterState.length > 0 ? ( + + + {tempOnly && ( + + + } + title={tempOnly.name()} + subheader={ + UnitMeasurement.any(um, user)) + )} + /> + } + titleTypographyProps={{ variant: "subtitle1" }} + /> + + )} + {heater && ( + + + } + title={heater.name()} + subheader={ + UnitMeasurement.create(um, user)) + )} + /> + } + titleTypographyProps={{ variant: "subtitle1" }} + /> + + )} + + + + {heaterStateWithTemps(tempOnly, heater)} + + + ) : ( + + + No measurements for selected timeframe + + + )} + + )} + + + {tempHum && ( + + {ambientTemps.length > 0 ? ( + + + } + title={tempHum.name()} + subheader={ + um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE + ) + .map(um => UnitMeasurement.any(um, user)) + )} + /> + } + titleTypographyProps={{ variant: "subtitle1" }} + /> + + { + changeXDomain(domain); + }} + multiGraphZoomOut + /> + + + ) : ( + + + No measurements for selected timeframe + + + )} + + )} + + {mutationGraphs()} + + + + + ); +} diff --git a/src/pages/Heater.tsx b/src/pages/Heater.tsx new file mode 100644 index 0000000..d4a0fa3 --- /dev/null +++ b/src/pages/Heater.tsx @@ -0,0 +1,534 @@ +import { + Box, + Button, + Card, + DialogActions, + DialogContent, + DialogTitle, + Grid2 as Grid, + Tooltip, + Typography +} from "@mui/material"; +import DeviceLinkDrawer from "common/DeviceLinkDrawer"; +import ObjectControls from "common/ObjectControls"; +import ObjectHeaterActions from "objectHeater/ObjectHeaterActions"; +import { useDeviceAPI, useGlobalState, useObjectHeaterAPI, useUserAPI } from "providers"; +import React, { useEffect, useState } from "react"; +import { useLocation } from "react-router"; +import PageContainer from "./PageContainer"; +import { ObjectHeater } from "models/ObjectHeater"; +//import { MatchParams } from "navigation/Routes"; +import { pond } from "protobuf-ts/pond"; +import { Component, Device, Scope } from "models"; +import { useSnackbar, useThemeType } from "hooks"; +import { quack } from "protobuf-ts/quack"; +import ObjectHeaterCharts from "objectHeater/ObjectHeaterCharts"; +import HeaterDarkIcon from "assets/components/heaterDark.png"; +import HeaterLightIcon from "assets/components/heaterLight.png"; +import Link from "@mui/icons-material/Link"; +import { LinkOff } from "@mui/icons-material"; +import { UnitMeasurement } from "models/UnitMeasurement"; +import TemperatureIcon from "component/TemperatureIcon"; +import FuelIcon from "products/CommonIcons/fuelIcon"; +import moment from "moment"; +import Warning from "@mui/icons-material/Warning"; +import { getTemperatureUnit } from "utils"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { useParams } from "react-router-dom"; + +export default function Heater() { + const [openLinkDrawer, setOpenLinkDrawer] = useState(false); + const { openSnack } = useSnackbar(); + const userAPI = useUserAPI(); + const heaterAPI = useObjectHeaterAPI(); + const deviceAPI = useDeviceAPI(); + const location = useLocation(); + //const match = useRouteMatch(); + //const heaterID = match.params.heaterID; + const heaterID = useParams<{ heaterKey: string }>()?.heaterKey ?? ""; + const [{ as, user }] = useGlobalState(); + const [permissions, setPermissions] = useState([]); + const [heater, setHeater] = useState(ObjectHeater.create()); + const [loadingDevs, setLoadingDevs] = useState(false); + const [devices, setDevices] = useState>( + new Map() + ); + const [displayDevice, setDisplayDevice] = useState(); + const [componentsByDevice, setComponentsByDevice] = useState>(); + const [displayedMutations, setDisplayedMutations] = useState([]); + const [heaterComp, setHeaterComp] = useState(); + const [heaterTempComp, setHeaterTempComp] = useState(); + const [siteTempComp, setSiteTempComp] = useState(); + const [fuelLevel, setFuelLevel] = useState("Unknown"); + const themeType = useThemeType(); + const [heaterStateUpToDate, setHeaterStateUpToDate] = useState(true); + const [heaterTempUpToDate, setHeaterTempUpToDate] = useState(true); + const [siteTempUpToDate, setSiteTempUpToDate] = useState(true); + const [xDomain, setXDomain] = useState(["dataMin", "dataMax"]); + const [unlinkDialogOpen, setUnlinkDialogOpen] = useState(false); + + const zoomOut = () => { + setXDomain(["dataMin", "dataMax"]); + }; + + const setComponents = (compDev: pond.ComprehensiveDevice) => { + compDev.components.forEach(component => { + let c = Component.any(component); + if ( + c.settings.type === quack.ComponentType.COMPONENT_TYPE_BOOLEAN_OUTPUT && + c.settings.subtype === quack.BooleanOutputSubtype.BOOLEAN_OUTPUT_SUBTYPE_HEATER + ) { + setHeaterComp(c); + if (c.lastMeasurement[0] && c.lastMeasurement[0].timestamps[0]) { + setHeaterStateUpToDate( + moment().diff(moment(c.lastMeasurement[0].timestamps[0]), "milliseconds") <= + c.settings.reportPeriodMs + ); + } + } + if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_TEMPERATURE) { + setHeaterTempComp(c); + if (c.lastMeasurement[0] && c.lastMeasurement[0].timestamps[0]) { + setHeaterTempUpToDate( + moment().diff(moment(c.lastMeasurement[0].timestamps[0]), "milliseconds") <= + c.settings.reportPeriodMs + ); + } + } + if (c.settings.type === quack.ComponentType.COMPONENT_TYPE_DHT) { + setSiteTempComp(c); + if (c.lastMeasurement[0] && c.lastMeasurement[0].timestamps[0]) { + setSiteTempUpToDate( + moment().diff(moment(c.lastMeasurement[0].timestamps[0]), "milliseconds") <= + c.settings.reportPeriodMs + ); + } + } + }); + }; + + useEffect(() => { + let key = heaterID; + let kind = "objectHeater"; + if (as) { + key = as; + kind = "team"; + } + userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => { + setPermissions(resp.permissions); + }); + }, [as, heaterID, userAPI, user]); + + useEffect(() => { + if (location.state && location.state.heater) { + setHeater(location.state.heater); + if (location.state.devices) { + let map: Map = new Map(); + let devMap = new Map(); + location.state.devices.forEach((compDev: pond.ComprehensiveDevice) => { + let device = Device.any(compDev.device); + let components = compDev.components.map(c => Component.any(c)); + devMap.set(device.id().toString(), pond.ComprehensiveDevice.fromObject(compDev)); + map.set(device.id().toString(), components); + setDisplayDevice(compDev); + setComponents(compDev); + }); + setDevices(devMap); + setComponentsByDevice(map); + setDisplayedMutations(location.state.heater.settings.mutations ?? []); + } + } else { + //if the heater was not carried from the heaters page, ie. page was reloaded, we will need to load the heater from the backend + heaterAPI + .getObjectHeater(heaterID) + .then(resp => { + let h = ObjectHeater.any(resp.data.heater); + setHeater(h); + setDisplayedMutations(h.settings.mutations ?? []); + }) + .catch(err => { + + }); + + if (!loadingDevs) { + setLoadingDevs(true); + deviceAPI + .list( + 100, + 0, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + true, + [heaterID], + ["objectHeater"] + ) + .then(resp => { + setLoadingDevs(false); + let map: Map = new Map(); + let devMap = new Map(); + resp.data.comprehensiveDevices.forEach(compDev => { + let device = Device.any(compDev.device); + let components = compDev.components.map(c => Component.any(c)); + devMap.set(device.id().toString(), pond.ComprehensiveDevice.fromObject(compDev)); + map.set(device.id().toString(), components); + setDisplayDevice(compDev); + setComponents(compDev); + }); + setDevices(devMap); + setComponentsByDevice(map); + }); + } + } + }, [location, heaterAPI, heaterID, deviceAPI]); //eslint-disable-line react-hooks/exhaustive-deps + + const disconnectDisplayDevice = () => { + if (displayDevice) { + let deviceID = displayDevice.device?.settings?.deviceId; + if (deviceID) { + heaterAPI + .updateLink(heater.key, "objectHeater", deviceID.toString(), "device", []) + .then(resp => { + if (deviceID) { + devices.delete(deviceID.toString()); + setDisplayDevice(undefined); + setComponentsByDevice(undefined); + setHeaterComp(undefined); + setHeaterTempComp(undefined); + setSiteTempComp(undefined); + setFuelLevel("Unknown"); + let updatedHeater = heater; + updatedHeater.settings.mutations = []; + heaterAPI + .updateObjectHeater(heater.key, heater.name, updatedHeater.settings) + .then(resp => { + setHeater(updatedHeater); + setDisplayedMutations([...updatedHeater.settings.mutations]); + }); + } + }); + } + } + }; + + const deviceDrawer = () => { + return ( + { + setOpenLinkDrawer(false); + }} + linkedDevices={devices} + updateLinkedDevices={(dev, linked) => { + let deviceID = dev.device?.settings?.deviceId; + if (deviceID) { + if (linked) { + heaterAPI + .updateLink(heater.key, "objectHeater", deviceID.toString(), "device", [ + "read", + "write", + "grant", + "revoke" + ]) + .then(resp => { + if (deviceID) { + devices.set(deviceID.toString(), dev); + setDisplayDevice(dev); + setOpenLinkDrawer(false); + openSnack("Device Linked to Heater"); + setComponents(dev); + let components: Map = new Map(); + components.set( + deviceID.toString(), + dev.components.map(c => Component.any(c)) + ); + setComponentsByDevice(components); + } + }); + } + } + }} + /> + ); + }; + + const displayHeaterState = () => { + let state = "Unknown"; + if (heaterComp) { + let lastReading = heaterComp.lastMeasurement[0]; + if (lastReading && lastReading.values.length > 0) { + if (lastReading.values[0].values[0] === 0) { + state = "Off"; + } else if (lastReading.values[0].values[0] === 1) { + state = "On"; + } + } + } + return ( + + {state} + {!heaterStateUpToDate && ( + + + + )} + + ); + }; + + const tempUnit = () => { + let unit = "°C"; + if (getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT) { + unit = "°F"; + } + return unit; + }; + + const displayHeaterTemp = () => { + let temp = "Unknown"; + if (heaterTempComp) { + let lastReading = UnitMeasurement.any(heaterTempComp.lastMeasurement[0], user); + if (lastReading && lastReading.values[0] && lastReading.values[0].values[0]) { + temp = lastReading.values[0].values[0].toFixed(2) + tempUnit(); + } + } + return ( + + + + {temp} + + {!heaterTempUpToDate && ( + + + + )} + + ); + }; + + const displaySiteTemp = () => { + let temp = "Unknown"; + if (siteTempComp) { + siteTempComp.lastMeasurement.forEach(um => { + if (um.type === quack.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) { + let lastReading = UnitMeasurement.any(um, user); + if (lastReading && lastReading.values[0] && lastReading.values[0].values[0]) { + temp = lastReading.values[0].values[0].toFixed(2) + tempUnit(); + } + } + }); + } + return ( + + + + {temp} + + {!siteTempUpToDate && ( + + + + )} + + ); + }; + + const unlinkDialog = () => { + return ( + { + setUnlinkDialogOpen(false); + }}> + Unlink Device + Remove device from heater? + + + + + + ); + }; + + const heaterInformation = () => { + return ( + + + + + {"heaterIcon"} + + {heater.name} + + + + + + + {!displayDevice ? ( + + ) : ( + + )} + + + + + {displayDevice ? ( + + + + {displayHeaterState()} + + + Heater State: + + + + + {displayHeaterTemp()} + + + Heater Temperature: + + + + + {displaySiteTemp()} + + + Site Temperature: + + + + + + + {fuelLevel} + + + + Fuel Level: + + + + ) : ( + No Connected Devices Found + )} + + + + ); + }; + + const deviceInformation = () => { + if (displayDevice) { + if (loadingDevs) { + return Loading Device; + } else { + return ( + + { + setFuelLevel(level); + }} + xDomain={xDomain} + changeXDomain={setXDomain} + /> + + ); + } + } else { + return ( + + + + Connect Device To Heater to View Data + + + + ); + } + }; + + return ( + + {deviceDrawer()} + {unlinkDialog()} + + {}} + immediateUpdate={newHeater => { + setHeater(newHeater); + setDisplayedMutations([...newHeater.settings.mutations]); + }} + /> + } + permissions={permissions} + devices={displayDevice && [Device.any(displayDevice.device)]} + /> + {heaterInformation()} + {deviceInformation()} + + + ); +} diff --git a/src/pages/Heaters.tsx b/src/pages/Heaters.tsx index b7eb35e..80b7471 100644 --- a/src/pages/Heaters.tsx +++ b/src/pages/Heaters.tsx @@ -32,45 +32,55 @@ export default function Heaters() { const [tablePage, setTablePage] = useState(0) const [pageSize, setPageSize] = useState(10) const [total, setTotal] = useState(0) + const [searchText, setSearchText] = useState("") const loadData = useCallback(() => { objectHeaterAPI - .listObjectHeatersPageData(500, 0, undefined, undefined, undefined, as) + .listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as) .then(resp => { setTotal(resp.data.total) setHeaters(resp.data.heaterData); }); - }, [objectHeaterAPI, as]); + }, [objectHeaterAPI, as, pageSize, tablePage, searchText]); useEffect(() => { loadData(); }, [loadData]); - const goToHeater = (heater: ObjectHeater, devices: pond.ComprehensiveDevice[]) => { - let path = "/objectHeaters/" + heater.key; - navigate(path, {state: { heater: heater, devices: devices}}) + const goToHeater = (row: pond.ObjectHeaterData) => { + let path = "/heaters/" + row.heater?.key; + navigate(path, {state: { heater: row.heater, devices: row.devices}}) + }; + + const handleChange = (event: any) => { + setPageSize(event.target.value); }; const columns = (): Column[] => { return [ { title: "Heater", + cellStyle: {padding: 2}, render: row => ({row.heater?.name}) }, { title: "Site", + cellStyle: {padding: 2}, render: row => ({row.site?.settings?.siteName}) }, { title: "Make", + cellStyle: {padding: 2}, render: row => ({row.heater?.settings?.make}) }, { title: "Model", + cellStyle: {padding: 2}, render: row => ({row.heater?.settings?.model}) }, { title: "Device", + cellStyle: {padding: 2}, render: row => { let display: string | number | undefined = "No Device Found" if (row.devices.length > 0) { @@ -89,13 +99,16 @@ export default function Heaters() { const desktopTable = () => { return ( + title={"Heaters"} page={tablePage} + setSearchText={(text) => {setSearchText(text)}} pageSize={pageSize} total={total} rows={heaters} columns={columns} setPage={(page)=>{setTablePage(page)}} - handleRowsPerPageChange={()=>{}} + handleRowsPerPageChange={handleChange} + onRowClick={goToHeater} /> ) };