From 1ec3058d45b29aca8f349a9f9b4b4b1151c5d28b Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 8 May 2025 16:45:42 -0600 Subject: [PATCH 1/6] added support page --- src/common/SmartBreadcrumb.tsx | 19 ++- src/device/DeviceActions.tsx | 8 +- src/navigation/Router.tsx | 5 + src/pages/DeviceSupport.tsx | 303 +++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 src/pages/DeviceSupport.tsx diff --git a/src/common/SmartBreadcrumb.tsx b/src/common/SmartBreadcrumb.tsx index 4d2c176..c931b79 100644 --- a/src/common/SmartBreadcrumb.tsx +++ b/src/common/SmartBreadcrumb.tsx @@ -7,6 +7,7 @@ import { LinkProps, Skeleton, Theme, + Typography, useTheme } from "@mui/material"; import { Replay } from "@mui/icons-material"; @@ -57,6 +58,7 @@ interface Props { paddingTop?: number; paddingBottom?: number; prependPaths?: string[]; + } interface LinkRouterProps extends LinkProps { @@ -289,7 +291,7 @@ export default function SmartBreadcrumb(props: Props) { if (prependPaths) pathnames.splice(pathnames.length - 1, 0, ...prependPaths); - pathnames.forEach((_value: any, index: any) => { + pathnames.forEach((value: any, index: any) => { const lastPath = index === pathnames.length - 1; const to = `/${pathnames.slice(0, index + 1).join("/")}`; @@ -310,6 +312,21 @@ export default function SmartBreadcrumb(props: Props) { links.push(linkComponent(to, label, lastPath)); } + if (value === "support") { + links.push( + + Support + + } + className={classes.chip} + /> + ) + } + }); return links; }; diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index 152ba70..baf2c49 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -24,7 +24,8 @@ import { AccountCircle as ObjectUsersIcon, SupervisedUserCircle as ObjectTeamsIcon, Sync as SyncDeviceIcon, - Wifi as WifiIcon + Wifi as WifiIcon, + Visibility } from "@mui/icons-material"; import { Skeleton } from "@mui/material"; // import Datadog from "assets/external/datadog.png"; @@ -498,6 +499,11 @@ export default function DeviceActions(props: Props) { const buttons = () => { return ( + {user.allowedTo("provision") && + navigate("support", { state: {device: device} })}> + + + } {canWrite && user.allowedTo("provision") && } {/* {!isShareableLink(match.params.deviceID) && ( */} import("pages/DeviceHistory")); const DevicePage = lazy(() => import("pages/Device")); @@ -103,6 +104,10 @@ export default function Router() { path="/:deviceID" // "/settings/basic" element={} /> + } + /> } diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx new file mode 100644 index 0000000..961cdaa --- /dev/null +++ b/src/pages/DeviceSupport.tsx @@ -0,0 +1,303 @@ +import { Box, Button, FormControl, Grid2 as Grid, MenuItem, Select, Tab, Tabs, TextField, Typography, useTheme } from "@mui/material"; +import PageContainer from "./PageContainer"; +import { pond } from "protobuf-ts/pond"; +import LogsDisplay from "common/LogsDisplay"; +import { useEffect, useState } from "react"; +import { useDeviceAPI, useSnackbar } from "hooks"; +import { useLocation, useParams } from "react-router-dom"; +import { Device } from "models"; +import { getContextKeys, getContextTypes } from "pbHelpers/Context"; +import SmartBreadcrumb from "common/SmartBreadcrumb"; + +interface TabPanelProps { + children?: React.ReactNode; + index: any; + value: any; +} + +function TabPanelMine(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +export default function DeviceSupport() { + const deviceID = +(useParams<{ deviceID: string }>()?.deviceID ?? "0"); + + const deviceAPI = useDeviceAPI() + const snackbar = useSnackbar() + const theme = useTheme() + + const { state } = useLocation(); + const [device, setDevice] = useState(state?.device ? Device.create(state.device) : Device.create()) + const [tabNumber, setTabNumber] = useState(0) + const [newCap, setNewCap] = useState(0); + const [datacap, setDatacap] = useState(); + const [isOver, setIsOver] = useState(undefined); + const [pauseCheck, setPauseCheck] = useState(undefined); + const [inputFirmware, setInputFirmware] = useState(""); + const [platform, setPlatform] = useState( + pond.DevicePlatform.DEVICE_PLATFORM_INVALID + ); + + useEffect(() => { + if (state?.device) return + deviceAPI.get(deviceID, undefined, getContextKeys(), getContextTypes()).then(resp => { + setDevice(Device.create(resp.data)) + }) + }, [deviceID, state?.device]) + + const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { + setTabNumber(newValue); + }; + + const resetQuackCount = () => { + deviceAPI.resetQuackCount(device.id()).then(resp => console.log(resp)); + }; + + const resetQuackCountTx = () => { + deviceAPI.resetQuackCountTx(device.id()).then(resp => console.log(resp)); + }; + + const resetQuackCountTx1000 = () => { + deviceAPI.resetQuackCountTx1000(device.id()).then(resp => console.log(resp)); + }; + + const clearPending = () => { + deviceAPI.clearPending(device.id()).then(resp => { + console.log(resp); + }); + }; + + const setFirmwareVersion = (firmwareVersion: string) => { + let status = device.status; + status.firmwareVersion = firmwareVersion; + deviceAPI.updateStatus(device.id(), status).then(() => { + snackbar.info("Firmware version set to " + firmwareVersion); + }); + }; + + const updateDevicePlatform = (firmwareVersion: pond.DevicePlatform) => { + let settings = device.settings; + settings.platform = firmwareVersion; + deviceAPI.update(device.id(), settings).then(() => { + snackbar.info("Platform set to " + firmwareVersion.toString()); + }); + }; + + const getDatacap = () => { + deviceAPI.getDatacap(device.id()).then(resp => { + //console.log(resp); + setDatacap(resp.data.overlimit); + }); + }; + + const updateDatacap = () => { + deviceAPI.setDatacap(device.id(), newCap).then(resp => { + console.log(resp); + }); + }; + + const checkLimit = () => { + deviceAPI.isOverLimit(device.id()).then(resp => { + //console.log(resp); + setIsOver(resp.data); + }); + }; + + const checkIfPaused = () => { + deviceAPI.isPaused(device.id()).then(resp => { + //console.log(resp); + setPauseCheck(resp.data); + }); + }; + + const pauseDevice = () => { + deviceAPI.pause(device.id()).then(resp => { + console.log(resp); + }); + }; + + const resumeDevice = () => { + deviceAPI.resume(device.id()).then(resp => { + console.log(resp); + }); + }; + + return ( + + + + + + + + + + + + General Options: + + + + + + + + + + + + + + setInputFirmware(e.currentTarget.value)} + /> + + + + + + + + + + + + + + + OPI Cable Functions: + + + + + + + + + + + + + + + + setNewCap(+e.target.value)} + /> + + + + + + + + + + + {datacap && datacap / 1000 / 1000}Mb + + + + + + + + + {isOver === undefined ? "Click to Check" : isOver ? "Over Limit" : "Under Limit"} + + + + + + + + + + {pauseCheck === undefined ? "Click to Check" : pauseCheck ? "Paused" : "Running"} + + + + + + + + + + + + History Not imported yet + {/* */} + + + ) +} \ No newline at end of file From 61bdff0cef483b809911dcb0b8a89377daf49cf0 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 12 May 2025 10:14:31 -0600 Subject: [PATCH 2/6] smart bread crumbs can be given a navigation state --- src/common/SmartBreadcrumb.tsx | 10 ++++++---- src/common/StatusSen5x.tsx | 4 ++-- src/pages/Device.tsx | 8 +++++++- src/pages/DeviceSupport.tsx | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/common/SmartBreadcrumb.tsx b/src/common/SmartBreadcrumb.tsx index c931b79..be1d0fe 100644 --- a/src/common/SmartBreadcrumb.tsx +++ b/src/common/SmartBreadcrumb.tsx @@ -12,7 +12,7 @@ import { } from "@mui/material"; import { Replay } from "@mui/icons-material"; import { useMobile } from "hooks"; -import { ReactNode, useEffect, useRef } from "react"; +import { ReactNode } from "react"; import { useLocation } from "react-router"; import { Link as RouterLink } from "react-router-dom"; import { or } from "utils/types"; @@ -58,12 +58,13 @@ interface Props { paddingTop?: number; paddingBottom?: number; prependPaths?: string[]; - + state?: any; } interface LinkRouterProps extends LinkProps { to: string; replace?: boolean; + state?: any; } const LinkRouter = (props: LinkRouterProps) => ; @@ -74,7 +75,7 @@ export default function SmartBreadcrumb(props: Props) { const location = useLocation(); const isMobile = useMobile(); const classes = useStyles(); - const { loading, reload } = props; + const { loading, reload, state } = props; const groupID = (): string => { let index = prependPaths?.indexOf("groups") @@ -316,7 +317,7 @@ export default function SmartBreadcrumb(props: Props) { links.push( Support @@ -341,6 +342,7 @@ export default function SmartBreadcrumb(props: Props) { color={lastPath ? "textPrimary" : "textSecondary"} variant="subtitle1" to={to} + state={state} sx={{ '&:hover': { color: getThemeType() === "dark" ? 'white' : "black" }}} className={classes.textOverflow}> {label} diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 262f595..912f128 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -176,12 +176,12 @@ export default function StatusPlenum(props: Props) { > - {device.status.sen5x?.voc.toFixed(1)}% + V: {device.status.sen5x?.voc.toFixed(1)}% - {device.status.sen5x?.nox.toFixed(1)}% + N: {device.status.sen5x?.nox.toFixed(1)}% diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 702285e..9282a50 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -30,6 +30,7 @@ export default function DevicePage() { const groupID = useParams<{ groupID: string }>()?.groupID ?? ""; const { state } = useLocation(); const [{ as, team, user }] = useGlobalState() + // console.log(state) const [device, setDevice] = useState(state?.device ? Device.create(state.device) : Device.create()) const [loading, setLoading] = useState(false) const [permissions, setPermissions] = useState([]) @@ -54,6 +55,11 @@ export default function DevicePage() { const loadDevice = () => { // console.log("load device page data") if (loading) return + console.log(state) + if (state?.device) { + console.log(Device.create(state.device)) + return + } setLoading(true) deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes(), as).then(resp => { let device = Device.any(resp.data.device) @@ -418,7 +424,7 @@ export default function DevicePage() { - + - + Date: Tue, 13 May 2025 10:52:40 -0600 Subject: [PATCH 3/6] support page caches page data to the bread crumbs for quick navigation back to device page --- src/device/DeviceActions.tsx | 18 ++++++++++++++++-- src/pages/Device.tsx | 22 ++++++++++++++++++---- src/pages/DeviceSupport.tsx | 35 +++++++++++++++++++++++++++-------- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index baf2c49..d988f7b 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -48,7 +48,7 @@ import { Component, Device, deviceScope, Interaction } from "models"; // import { isShareableLink } from "pbHelpers/Device"; import { pond } from "protobuf-ts/pond"; import { useGlobalState } from "providers"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; // import { useHistory, useRouteMatch } from "react-router"; import { isOffline } from "utils/environment"; import { or } from "utils/types"; @@ -72,6 +72,7 @@ import Connection from "./Connection"; import ComponentOrder from "component/ComponentOrder"; import ArcGISDeviceData from "./ArcGISDeviceData"; import UpgradeDevice from "./UpgradeDevice"; +import { DevicePageData } from "pages/Device"; const useStyles = makeStyles((_theme: Theme) => { // const isMobile = useMobile() @@ -170,6 +171,19 @@ export default function DeviceActions(props: Props) { isJsonDataDialogOpen: false }); + const [devicePageData, setDevicePageData] = useState(undefined); + + useEffect(() => { + let newPageData: DevicePageData = { + device: device, + components: components, + interactions: interactions, + preferences: preferences, + permissions: permissions, + } + setDevicePageData(newPageData) + }, [device, components, interactions, preferences, permissions]) + const openDialog = (target: keyof DialogState) => { let updatedDialogState = cloneDeep(dialogState); updatedDialogState[target] = true; @@ -500,7 +514,7 @@ export default function DeviceActions(props: Props) { return ( {user.allowedTo("provision") && - navigate("support", { state: {device: device} })}> + navigate("support", { state: {device: device, devicePageData: devicePageData }})}> } diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 9282a50..2a03a10 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -22,6 +22,15 @@ import { or } from "utils"; import ComponentDiagnostics from "component/ComponentDiagnostics"; import DeviceWizard from "device/DeviceWizard"; +export interface DevicePageData { + device: Device; + components: Component[]; + interactions: Interaction[]; + permissions: pond.Permission[]; + preferences: pond.DevicePreferences; + // componentPreferences: Map +} + export default function DevicePage() { const deviceAPI = useDeviceAPI() const snackbar = useSnackbar() @@ -52,16 +61,21 @@ export default function DevicePage() { const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false) + // const [devicePageData, setDevicePageData] = useState(pond.GetDevicePageDataResponse.create()) + const loadDevice = () => { - // console.log("load device page data") if (loading) return - console.log(state) - if (state?.device) { - console.log(Device.create(state.device)) + if (state?.devicePageData) { + setDevice(Device.create(state.devicePageData.device)) + setComponents(state.devicePageData.components.map((comp: pond.Component) => Component.create(comp))) + setInteractions(state.devicePageData.interactions.map((inter: pond.Interaction) => Interaction.create(inter))) + setPreferences(state.devicePageData.preferences) + setPermissions(state.devicePageData.permissions) return } setLoading(true) deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes(), as).then(resp => { + // setDevicePageData(resp.data) let device = Device.any(resp.data.device) // console.log(resp.data.device) setDevice(device) diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index a241223..4e47a07 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -5,9 +5,10 @@ import LogsDisplay from "common/LogsDisplay"; import { useEffect, useState } from "react"; import { useDeviceAPI, useSnackbar } from "hooks"; import { useLocation, useParams } from "react-router-dom"; -import { Device } from "models"; +import { Component, Device, Interaction } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import SmartBreadcrumb from "common/SmartBreadcrumb"; +import { DevicePageData } from "./Device"; interface TabPanelProps { children?: React.ReactNode; @@ -37,7 +38,8 @@ export default function DeviceSupport() { const theme = useTheme() const { state } = useLocation(); - const [device, setDevice] = useState(state?.device ? Device.create(state.device) : Device.create()) + const [device, setDevice] = useState(Device.create()) + const [devicePageData, setDevicePageData] = useState(state?.devicePageData ? state.devicePageData : undefined) const [tabNumber, setTabNumber] = useState(0) const [newCap, setNewCap] = useState(0); const [datacap, setDatacap] = useState(); @@ -48,12 +50,29 @@ export default function DeviceSupport() { pond.DevicePlatform.DEVICE_PLATFORM_INVALID ); - useEffect(() => { - if (state?.device) return - deviceAPI.get(deviceID, undefined, getContextKeys(), getContextTypes()).then(resp => { - setDevice(Device.create(resp.data)) + // useEffect(() => { + // if (state?.device) return + // deviceAPI.get(deviceID, undefined, getContextKeys(), getContextTypes()).then(resp => { + // setDevice(Device.create(resp.data)) + // }) + // }, [deviceID, state?.device]) + + useEffect(() => { + if (state?.devicePageData) { + setDevice(Device.create(state?.devicePageData.device)) + return + } + deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => { + setDevice(resp.data?.device ? Device.create(resp.data?.device) : Device.create()) + setDevicePageData({ + device: resp.data.device ? Device.create(resp.data.device) : Device.create(), + components: resp.data.components.map(comp => Component.create(comp)), + interactions: resp.data.interactions.map(interaction => Interaction.create(interaction)), + permissions: resp.data.permissions, + preferences: resp.data.user?.preferences as pond.DevicePreferences + }) }) - }, [deviceID, state?.device]) + }, [deviceID, state?.devicePageData]) const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { setTabNumber(newValue); @@ -134,7 +153,7 @@ export default function DeviceSupport() { return ( - + Date: Tue, 13 May 2025 11:46:44 -0600 Subject: [PATCH 4/6] added device actions to support page --- src/device/DeviceActions.tsx | 6 +++- src/pages/DeviceSupport.tsx | 66 ++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index d988f7b..8356b6a 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -510,10 +510,14 @@ export default function DeviceActions(props: Props) { ); }; + const showSupport = () => { + return user.allowedTo("provision") && !location.pathname.includes("support") + } + const buttons = () => { return ( - {user.allowedTo("provision") && + {showSupport() && navigate("support", { state: {device: device, devicePageData: devicePageData }})}> diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index 4e47a07..2e873e6 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -9,6 +9,11 @@ import { Component, Device, Interaction } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import SmartBreadcrumb from "common/SmartBreadcrumb"; import { DevicePageData } from "./Device"; +import AddComponentManualDialog from "component/AddComponentManualDialog"; +import { useGlobalState } from "providers"; +import DeviceActions from "device/DeviceActions"; +import { cloneDeep } from "lodash"; +import { FindAvailablePositions } from "pbHelpers/DeviceAvailability"; interface TabPanelProps { children?: React.ReactNode; @@ -38,6 +43,7 @@ export default function DeviceSupport() { const theme = useTheme() const { state } = useLocation(); + const [{ user, as }] = useGlobalState() const [device, setDevice] = useState(Device.create()) const [devicePageData, setDevicePageData] = useState(state?.devicePageData ? state.devicePageData : undefined) const [tabNumber, setTabNumber] = useState(0) @@ -49,6 +55,8 @@ export default function DeviceSupport() { const [platform, setPlatform] = useState( pond.DevicePlatform.DEVICE_PLATFORM_INVALID ); + const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false) + const [loading, setLoading] = useState(false) // useEffect(() => { // if (state?.device) return @@ -62,6 +70,7 @@ export default function DeviceSupport() { setDevice(Device.create(state?.devicePageData.device)) return } + setLoading(true) deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => { setDevice(resp.data?.device ? Device.create(resp.data?.device) : Device.create()) setDevicePageData({ @@ -71,7 +80,9 @@ export default function DeviceSupport() { permissions: resp.data.permissions, preferences: resp.data.user?.preferences as pond.DevicePreferences }) - }) + }).finally(() => { + setLoading(false) + }) }, [deviceID, state?.devicePageData]) const handleChange = (_event: React.ChangeEvent<{}>, newValue: number) => { @@ -151,9 +162,47 @@ export default function DeviceSupport() { }); }; + const toggleNotificationPreference = () => { + let updatedPreferences = cloneDeep(devicePageData.preferences); + updatedPreferences.notify = !devicePageData.preferences.notify; + let updatedDevicePageData = cloneDeep(devicePageData) + updatedDevicePageData.preferences = updatedPreferences + deviceAPI + .updatePreferences(deviceID, updatedPreferences, getContextKeys(), getContextTypes(), as) + .then(() => setDevicePageData(updatedDevicePageData)) + .catch(() => { + snackbar.error( + "Error occured while " + + (devicePageData.preferences.notify ? "enabling" : "disabling") + + " notifications" + ); + }); + }; + return ( - + + + + + + + {}} + availablePositions={new Map()} + availableOffsets={new Map()} + components={[...devicePageData.components.map(comp => Component.any(comp))]} + interactions={devicePageData.interactions.map(inter => Interaction.any(inter))} + /> + + + {/* */} + + + setAddComponentManualDialogOpen(false)} + /> + {user.hasFeature("developer") === true && + + } + Date: Tue, 13 May 2025 11:48:53 -0600 Subject: [PATCH 5/6] available positions is known to device actions in support page --- src/pages/Device.tsx | 12 ------------ src/pages/DeviceSupport.tsx | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 2a03a10..911e9f0 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -441,16 +441,6 @@ export default function DevicePage() { - setAddComponentManualDialogOpen(false)} - /> - {user.hasFeature("developer") === true && - - } {}} - availablePositions={new Map()} + availablePositions={FindAvailablePositions(devicePageData.components, device.settings.product).availability} availableOffsets={new Map()} components={[...devicePageData.components.map(comp => Component.any(comp))]} interactions={devicePageData.interactions.map(inter => Interaction.any(inter))} From f72f5cd1063504f5f3b15e5c97420a86015d6d05 Mon Sep 17 00:00:00 2001 From: Carter Date: Tue, 13 May 2025 12:02:51 -0600 Subject: [PATCH 6/6] shows loading screen if devicePageData has not loaded in yet --- src/pages/DeviceSupport.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pages/DeviceSupport.tsx b/src/pages/DeviceSupport.tsx index c540fc0..4cccac6 100644 --- a/src/pages/DeviceSupport.tsx +++ b/src/pages/DeviceSupport.tsx @@ -14,6 +14,7 @@ import { useGlobalState } from "providers"; import DeviceActions from "device/DeviceActions"; import { cloneDeep } from "lodash"; import { FindAvailablePositions } from "pbHelpers/DeviceAvailability"; +import LoadingScreen from "app/LoadingScreen"; interface TabPanelProps { children?: React.ReactNode; @@ -179,6 +180,12 @@ export default function DeviceSupport() { }); }; + if (!devicePageData) { + return ( + + ) + } + return (