Merge branch 'staging_environment' into gate_page_fixes

This commit is contained in:
csawatzky 2025-05-14 14:53:08 -06:00
commit 62cc0a2e97
21 changed files with 2716 additions and 29 deletions

View file

@ -37,6 +37,11 @@ function App() {
faviconDocument.href = "BrandXTechnologies/favicon.ico"
})
// Set title
const titleElement = document.getElementById("title-id") as HTMLElement;
if (titleElement) titleElement.textContent = whiteLabel.name;
let domain: string | undefined = import.meta.env.VITE_AUTH0_CLIENT_DOMAIN;
let audience: string | undefined = import.meta.env.VITE_AUTH0_AUDIENCE;

View file

@ -72,14 +72,14 @@ const useStyles = makeStyles((theme: Theme) => {
marginLeft: theme.spacing(0.05),
marginRight: theme.spacing(0.05),
"&:hover": {
backgroundColor: getSecondaryColour(), //use the colour of the whitelabel
//backgroundColor: "gold",
//backgroundColor: getSecondaryColour(), //use the colour of the whitelabel
backgroundColor: "gold", //for now Dustin just wants it to be gold
color: "black",
}
},
buttonToggled: {
backgroundColor: getSecondaryColour(), //use the colour of the whitelabel
//backgroundColor: "gold",
//backgroundColor: getSecondaryColour(), //use the colour of the whitelabel
backgroundColor: "gold", //for now Dustin just wants it to be gold
color: "black",
borderRadius: 24,
fontWeight: "bold"

View file

@ -7,11 +7,12 @@ import {
LinkProps,
Skeleton,
Theme,
Typography,
useTheme
} 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";
@ -57,11 +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) => <Link {...props} component={RouterLink as any} />;
@ -72,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")
@ -289,7 +292,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 +313,21 @@ export default function SmartBreadcrumb(props: Props) {
links.push(linkComponent(to, label, lastPath));
}
if (value === "support") {
links.push(
<Chip
key={"support-chip"}
variant={"filled"}
label={
<Typography variant="subtitle1" color="textPrimary">
Support
</Typography>
}
className={classes.chip}
/>
)
}
});
return links;
};
@ -324,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}

View file

@ -176,12 +176,12 @@ export default function StatusPlenum(props: Props) {
>
<Grid2>
<Typography variant="body2" style={{ color: blue[300]}}>
{device.status.sen5x?.voc.toFixed(1)}%
V: {device.status.sen5x?.voc.toFixed(1)}%
</Typography>
</Grid2>
<Grid2>
<Typography variant="body2" style={{ color: blue[400]}}>
{device.status.sen5x?.nox.toFixed(1)}%
N: {device.status.sen5x?.nox.toFixed(1)}%
</Typography>
</Grid2>
</Grid2>

View file

@ -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";
@ -47,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";
@ -71,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()
@ -169,6 +171,19 @@ export default function DeviceActions(props: Props) {
isJsonDataDialogOpen: false
});
const [devicePageData, setDevicePageData] = useState<DevicePageData | undefined>(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;
@ -495,9 +510,18 @@ export default function DeviceActions(props: Props) {
);
};
const showSupport = () => {
return user.allowedTo("provision") && !location.pathname.includes("support")
}
const buttons = () => {
return (
<React.Fragment>
{showSupport() && <Tooltip title="Support">
<IconButton onClick={() => navigate("support", { state: {device: device, devicePageData: devicePageData }})}>
<Visibility />
</IconButton>
</Tooltip>}
{canWrite && user.allowedTo("provision") && <UpgradeDevice device={device} />}
{/* {!isShareableLink(match.params.deviceID) && ( */}
<NotificationButton

View file

@ -8,6 +8,7 @@ import { ErrorBoundary } from "react-error-boundary";
import { getWhitelabel } from "services/whiteLabel";
import Ventilation from "pages/VentEditor";
import { useGlobalState } from "providers";
import DeviceSupport from "pages/DeviceSupport";
const DeviceHistory = lazy(() => import("pages/DeviceHistory"));
const DevicePage = lazy(() => import("pages/Device"));
@ -103,6 +104,10 @@ export default function Router() {
path="/:deviceID" // "/settings/basic"
element={<DevicePage />}
/>
<Route
path="/:deviceID/support" // "/settings/basic"
element={<DeviceSupport />}
/>
<Route
path="/:deviceID/components/:componentID" // "/settings/basic"
element={<DeviceComponent />}

View file

@ -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<string, pond.DeviceComponentPreferences>
}
export default function DevicePage() {
const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar()
@ -30,6 +39,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<Device>(state?.device ? Device.create(state.device) : Device.create())
const [loading, setLoading] = useState(false)
const [permissions, setPermissions] = useState<pond.Permission[]>([])
@ -51,11 +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
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)
@ -418,19 +438,9 @@ export default function DevicePage() {
<PageContainer padding={isMobile ? 0 : 2}>
<Grid container justifyContent={"space-between"}>
<Grid>
<SmartBreadcrumb />
<SmartBreadcrumb deviceName={device.name()} />
</Grid>
<Grid>
<AddComponentManualDialog
device={device.id()}
open={addComponentManualDialogOpen}
onClose={() => setAddComponentManualDialogOpen(false)}
/>
{user.hasFeature("developer") === true &&
<Button onClick={() => setAddComponentManualDialogOpen(true)}>
Manual Comp
</Button>
}
<DeviceActions
device={device}
isPaused={false}
@ -449,8 +459,6 @@ export default function DevicePage() {
<DeviceOverview
device={device}
components={[...components.values()]}
// components={[]}
// components={components}
usage={getUsage()}
loading={loading}
groupID={parseInt(groupID)}

393
src/pages/DeviceSupport.tsx Normal file
View file

@ -0,0 +1,393 @@
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 { 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";
import LoadingScreen from "app/LoadingScreen";
import DeviceOverview from "device/DeviceOverview";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanelMine(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
{...other}>
{value === index && <>{children}</>}
</div>
);
}
export default function DeviceSupport() {
const deviceID = +(useParams<{ deviceID: string }>()?.deviceID ?? "0");
const deviceAPI = useDeviceAPI()
const snackbar = useSnackbar()
const theme = useTheme()
const { state } = useLocation();
const [{ user, as }] = useGlobalState()
const [device, setDevice] = useState<Device>(Device.create())
const [devicePageData, setDevicePageData] = useState<DevicePageData>(state?.devicePageData ? state.devicePageData : undefined)
const [tabNumber, setTabNumber] = useState(0)
const [newCap, setNewCap] = useState(0);
const [datacap, setDatacap] = useState<number>();
const [isOver, setIsOver] = useState(undefined);
const [pauseCheck, setPauseCheck] = useState(undefined);
const [inputFirmware, setInputFirmware] = useState<string>("");
const [platform, setPlatform] = useState<pond.DevicePlatform>(
pond.DevicePlatform.DEVICE_PLATFORM_INVALID
);
const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false)
const [loading, setLoading] = useState(false)
// 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
}
setLoading(true)
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
})
}).finally(() => {
setLoading(false)
})
}, [deviceID, state?.devicePageData])
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);
});
};
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"
);
});
};
if (!devicePageData) {
return (
<LoadingScreen message="Getting device data"/>
)
}
return (
<PageContainer>
<Grid container justifyContent={"space-between"}>
<Grid>
<SmartBreadcrumb deviceName={device.name()} state={{devicePageData: devicePageData}} />
</Grid>
<Grid>
<DeviceActions
device={device}
isPaused={false}
isLoading={loading}
permissions={devicePageData.permissions}
preferences={devicePageData.preferences}
toggleNotificationPreference={toggleNotificationPreference}
refreshCallback={() => {}}
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))}
/>
</Grid>
</Grid>
{/* <SmartBreadcrumb deviceName={device.name()} state={{devicePageData: devicePageData}} /> */}
<DeviceOverview device={device} components={devicePageData.components}/>
<br/>
<Tabs
style={{ position: "sticky" }}
value={tabNumber}
onChange={handleChange}
centered
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
<Tab label="Control Center" />
<Tab label="Hologram" />
<Tab label="DataDog" />
<Tab label="DuckData" />
</Tabs>
<TabPanelMine value={tabNumber} index={0}>
<Box>
<Typography gutterBottom variant="h5">
General Options:
</Typography>
<Grid container direction="row">
<Grid>
<Button color="primary" onClick={resetQuackCount}>
Reset Count Rx (safe, use when quack count errors)
</Button>
<Button color="primary" onClick={resetQuackCountTx}>
Reset Count Tx, Are you sure (ONLY use when device not responding)
</Button>
<Button color="primary" onClick={resetQuackCountTx1000}>
Reset Count Tx - 1000, Are you sure (ONLY use when device not responding)
</Button>
</Grid>
<Grid>
<Button color="primary" onClick={clearPending}>
Clear Pending
</Button>
</Grid>
</Grid>
<Grid>
<AddComponentManualDialog
device={device.id()}
open={addComponentManualDialogOpen}
onClose={() => setAddComponentManualDialogOpen(false)}
/>
{user.hasFeature("developer") === true &&
<Button onClick={() => setAddComponentManualDialogOpen(true)}>
Manual Comp
</Button>
}
</Grid>
<Grid container direction="row">
<Grid>
<TextField
value={inputFirmware}
onChange={e => setInputFirmware(e.currentTarget.value)}
/>
<Button color="primary" onClick={() => setFirmwareVersion(inputFirmware)}>
Set Firmware
</Button>
</Grid>
</Grid>
<Grid container direction="row">
<Grid>
<FormControl>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={platform}
label="Platform"
onChange={(event: any) => setPlatform(event.target.value)}>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_PHOTON}>Photon</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON}>
Electron
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR}>
V2 Cellular
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3}>
V2 Wifi S3
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK}>
V2 Cellular Black
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN}>
V2 Cellular Green
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE}>
V2 Wifi Blue
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE}>
V2 Cellular Blue
</MenuItem>
</Select>
</FormControl>
<Button color="primary" onClick={() => updateDevicePlatform(platform)}>
Set Platform
</Button>
</Grid>
</Grid>
</Box>
<Box marginTop={theme.spacing(0.5)}>
<Typography gutterBottom variant="h5">
OPI Cable Functions:
</Typography>
<Grid container direction="row">
<Grid>
<Button color="primary">Cable Reprogrammer</Button>
</Grid>
<Grid>
<Button color="primary">Cable Diagnostic</Button>
</Grid>
</Grid>
</Box>
</TabPanelMine>
<TabPanelMine value={tabNumber} index={1}>
<Grid container direction="column">
<Grid container direction="row">
<Grid>
<TextField
type="number"
value={newCap}
onChange={e => setNewCap(+e.target.value)}
/>
</Grid>
<Grid>
<Button onClick={updateDatacap} color="primary">
Set New Datacap
</Button>
</Grid>
</Grid>
<Grid container direction="row" alignItems="center">
<Grid>
<Button onClick={getDatacap} color="primary">
DataCap
</Button>
</Grid>
<Grid>
<Box margin={3}>{datacap && datacap / 1000 / 1000}Mb</Box>
</Grid>
</Grid>
<Grid container direction="row" alignItems="center">
<Grid>
<Button onClick={checkLimit} color="primary">
OverLimit
</Button>
</Grid>
<Grid>
<Box margin={3}>
{isOver === undefined ? "Click to Check" : isOver ? "Over Limit" : "Under Limit"}
</Box>
</Grid>
</Grid>
<Grid container direction="row" alignItems="center">
<Grid>
<Button onClick={checkIfPaused} color="primary">
Am I Paused
</Button>
</Grid>
<Grid>
<Box margin={3}>
{pauseCheck === undefined ? "Click to Check" : pauseCheck ? "Paused" : "Running"}
</Box>
</Grid>
</Grid>
</Grid>
<Button onClick={pauseDevice} color="primary">
Pause Me
</Button>
<Button onClick={resumeDevice} color="primary">
Get Me Goin
</Button>
</TabPanelMine>
<TabPanelMine value={tabNumber} index={2}>
<LogsDisplay deviceID={deviceID} />
</TabPanelMine>
<TabPanelMine value={tabNumber} index={3}>
History Not imported yet
{/* <CompleteHistory device={device} /> */}
</TabPanelMine>
</PageContainer>
)
}

View file

@ -70,7 +70,7 @@ interface WhiteLabel {
// };
const STAGING_WHITELABEL: WhiteLabel = {
name: import.meta.env.VITE_APP_WEBSITE_TITLE,
name: "Staging",
primaryColour: import.meta.env.VITE_APP_PRIMARY_COLOUR,
secondaryColour: import.meta.env.VITE_APP_SECONDARY_COLOUR,
signatureColour: import.meta.env.VITE_APP_SIGNATURE_COLOUR,
@ -276,7 +276,7 @@ export function getWhitelabel(): WhiteLabel {
if (window.location.origin.includes("bxt-dev")) {
return BXT_WHITE_LABEL;
}
if (window.location.origin.includes("staging")) {
if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') {
return STAGING_WHITELABEL;
}
const whiteLabelKeys = Array.from(whitelabels.keys());