Merge branch 'staging_environment' into hybrid_inventory_control

This commit is contained in:
csawatzky 2025-05-16 09:24:23 -06:00
commit 3b8a47e760
63 changed files with 4307 additions and 983 deletions

15
.env
View file

@ -8,16 +8,15 @@ VITE_AUTH0_AUDIENCE=api.brandxtech.ca
VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1
VITE_AUTH0_STAGING_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP
# VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP
# VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca
# VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com
# VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1
# VITE_APP_WS_URL=ws://stagingapi.brandxtech.ca/v1/live
# VITE_APP_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com
# VITE_APP_AUTH0_AUDIENCE=stagingapi.brandxtech.ca
VITE_AUTH0_BXT_CLIENT_ID=sLnqOu40uWfQT1lYSDYj2wYmlLEHRB74
VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd
VITE_AUTH0_ADAPTIVE_CONSTRUCTION_CLIENT_ID=32rABabJzXRvJiWivTmeKFgwFiqh4ok7
VITE_AUTH0_AEROGROW_CLIENT_ID=KHl9ooUt1nia1RYw5n224dyggCXdbsSd
VITE_AUTH0_MIVENT_CLIENT_ID=VNALE7RW6l3dY5uYcxgwElZV0lcT25Fg
VITE_AUTH0_OMNIAIR_CLIENT_ID=IblmarD8wFafiD6doxTmOHQ6Bx3L9wWl
#Branding (Default theme)
VITE_APP_WEBSITE_TITLE="Adaptive Dashboard"
VITE_APP_PRIMARY_COLOUR=blue
VITE_APP_SECONDARY_COLOUR=blueGrey
VITE_APP_SIGNATURE_COLOUR="#323232"
VITE_APP_SIGNATURE_COLOUR="#323232"

View file

@ -13,8 +13,8 @@ dependencies:production:
only:
refs:
- master
before_script:
- npm -g config set user root
# before_script:
# - npm -g config set user root
script:
- npm ci
cache:
@ -28,17 +28,17 @@ dependencies:production:
paths:
- node_modules/
test:
image: node:$NODE_VERSION
stage: setup
dependencies:
- dependencies:production
only:
- master
before_script:
- npm run lint
script:
- CI=true npm run test
# test:
# image: node:$NODE_VERSION
# stage: setup
# dependencies:
# - dependencies:production
# only:
# - master
# before_script:
# - npm run lint
# script:
# - CI=true npm run test
setup:production:
image: node:$NODE_VERSION
@ -197,6 +197,7 @@ staging:deploy:
DEPLOY_IMAGE: ${CI_REGISTRY}/brandx/bxt-app/production:staging
DOCKER_HOST: ssh://deploy@staging.brandxtech.ca
DEPLOY_TARGET: deploy@staging.brandxtech.ca
VITE_MAPBOX_ACCESS_TOKEN: $MAPBOX_ACCESS_TOKEN
only:
- staging_environment
environment:
@ -206,6 +207,7 @@ staging:deploy:
- "apk --update add openssh-client"
- "eval $(ssh-agent -s)"
- "echo \"$STAGING_SSH_PRIV_KEY\" | tr -d '\r' | ssh-add - "
- "echo \"$MAPBOX_ACCESS_TOKEN\""
# - "ssh -Mo StrictHostKeyChecking=no -S deploy-ctrl-socket -fnNTL /var/run/docker.sock:/var/run/docker.sock ${DEPLOY_TARGET}"
# - "ssh -S deploy-ctrl-socket -O check ${DEPLOY_TARGET}"
- mkdir -p ~/.ssh

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" id="favicon-link" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Adaptive Dashboard</title>
<title id="title-id">Adaptive Dashboard</title>
<link rel="manifest" id="manifest-link" />
</head>
<body>

765
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,16 +4,16 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "vite",
"start": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 VITE_APP_WS_URL=ws://api.brandxtech.ca/v1/live vite",
"start-local": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=http://localhost:50052/v1 VITE_APP_WS_URL=ws://localhost:50052/v1/live VITE_APP_BILLING_URL=http://localhost:50053/v1 VITE_APP_RECLUSE_URL=http://localhost:50054/v1 VITE_APP_GITLAB_URL=http://localhost:50055/v1 vite",
"start-dev": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 vite",
"start-staging": "VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=ws://stagingapi.brandxtech.ca/v1/live VITE_APP_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_AUTH0_AUDIENCE=stagingapi.brandxtech.ca vite",
"start-staging": "VITE_LOCAL_STAGING=true VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=ws://stagingapi.brandxtech.ca/v1/live VITE_APP_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_APP_AUTH0_AUDIENCE=stagingapi.brandxtech.ca vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"build:staging": "VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=wss://stagingapi.brandxtech.ca/v1/live NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_STAGING} VITE_APP_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_APP_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_APP_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_APP_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_APP_CNHI_SCOPES=${CNHI_SCOPES} VITE_APP_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_APP_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_APP_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_APP_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_APP_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_APP_JD_SCOPES=${JD_SCOPES} VITE_APP_JD_STATE=${JD_STATE} vite build",
"build:development": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_APP_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_APP_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_APP_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_APP_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_APP_CNHI_SCOPES=${CNHI_SCOPES} VITE_APP_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_APP_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_APP_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_APP_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_APP_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_APP_JD_SCOPES=${JD_SCOPES} VITE_APP_JD_STATE=${JD_STATE} vite build",
"build:production": "NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_APP_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_APP_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_APP_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_APP_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_APP_CNHI_SCOPES=${CNHI_SCOPES} VITE_APP_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_APP_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_APP_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_APP_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_APP_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_APP_JD_SCOPES=${JD_SCOPES} VITE_APP_JD_STATE=${JD_STATE} vite build",
"build:staging": "VITE_AUTH0_CLIENT_DOMAIN=adaptivestaging.us.auth0.com VITE_AUTH0_AUDIENCE=stagingapi.brandxtech.ca VITE_AUTH0_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP VITE_APP_API_URL=https://stagingapi.brandxtech.ca/v1 VITE_APP_WS_URL=wss://stagingapi.brandxtech.ca/v1/live NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_STAGING} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP={OPEN_WEATHERMAP} vite build",
"build:development": "VITE_AUTH0_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_AUDIENCE=bxt-dev.api.adaptiveagriculture.ca VITE_APP_API_URL=https://bxt-dev.api.adaptiveagriculture.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_DEVELOPMENT} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP={OPEN_WEATHERMAP} vite build",
"build:production": "VITE_AUTH0_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_APP_API_URL=https://api.brandxtech.ca/v1 NODE_OPTIONS=--max_old_space_size=4096 VITE_APP_GOOGLE_API_KEY=${GOOGLE_API_KEY} VITE_APP_STRIPE_PUBLIC_KEY=${STRIPE_PUBLIC_KEY_PRODUCTION} VITE_MAPBOX_ACCESS_TOKEN=${MAPBOX_ACCESS_TOKEN} VITE_CNHI_CLIENT_ID=${CNHI_CLIENT_ID} VITE_CNHI_AUTHORIZE_URL=${CNHI_AUTHORIZE_URL} VITE_CNHI_REDIRECT_URI=${CNHI_REDIRECT_URI} VITE_CNHI_SCOPES=${CNHI_SCOPES} VITE_CNHI_CONNECTION=${CNHI_CONNECTION} VITE_CNHI_AUDIENCE=${CNHI_AUDIENCE} VITE_APP_IMAGE4IO_USERNAME=${IMAGE4IO_USERNAME} VITE_APP_IMAGE4IO_PASSWORD=${IMAGE4IO_PASSWORD} VITE_JD_CLIENT_ID=${JD_CLIENT_ID} VITE_JD_AUTHORIZE_URL=${JD_AUTHORIZE_URL} VITE_JD_REDIRECT_URI=${JD_REDIRECT_URI} VITE_JD_SCOPES=${JD_SCOPES} VITE_JD_STATE=${JD_STATE} VITE_OPEN_WEATHERMAP={OPEN_WEATHERMAP} vite build",
"build:offline": "npx env-cmd offline,whitelabel npm run build",
"test": "vitest"
},
@ -39,7 +39,6 @@
"@turf/area": "^7.2.0",
"@turf/turf": "^7.2.0",
"@types/classnames": "^2.3.0",
"@types/mapbox-gl": "^3.4.1",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"deep-object-diff": "^1.1.9",
@ -80,6 +79,7 @@
"@types/lodash": "^4.17.13",
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
"@types/mapbox-gl": "^3.4.1",
"@types/react": "^18.3.10",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-color": "^3.0.13",

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/Staging/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,26 @@
{
"name": "Brand X Technologies",
"short_name": "BXT",
"icons": [
{
"src": "favicon.ico",
"sizes": "48x48",
"type": "image/x-icon"
},
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "/",
"theme_color": "#272727",
"background_color": "#424242",
"display": "standalone",
"orientation": "any"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -37,19 +37,16 @@ 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;
// let client_id = whiteLabel.auth0ClientId
let client_id = import.meta.env.VITE_AUTH0_CLIENT_ID;
// if (!client_id) client_id = import.meta.env.VITE_AUTH0_CLIENT_ID;
//check the url for a code before auth0 causes a login re-direct
// if (window.location.pathname !== "/callback") {
// //set the code into local storage
// let code = new URLSearchParams(window.location.search).get("code");
// sessionStorage.setItem("code", code || "")
// }
let client_id = whiteLabel.auth0ClientId
if (!client_id) client_id = import.meta.env.VITE_AUTH0_CLIENT_ID;
const skipCallbacks = [
"/johndeere",

View file

@ -1,5 +1,6 @@
import { Box, Theme, ToggleButton, ToggleButtonGroup, darken } from "@mui/material";
import { makeStyles, withStyles } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
import { ExtractMoisture } from "grain";
import { GrainCable } from "models/GrainCable";
import { describeMeasurement } from "pbHelpers/MeasurementDescriber";
@ -212,54 +213,54 @@ export default function BinSVGV2(props: Props) {
const [extraDetails, setExtraDetails] = useState<"temps" | "hums">("temps");
const [nodeWarning, setNodeWarning] = useState(false);
const StyledToggleButtonGroup = withStyles(theme => ({
grouped: {
margin: theme.spacing(-0.5),
border: "none",
padding: theme.spacing(1),
"&:not(:first-child):not(:last-child)": {
borderRadius: 24,
marginRight: theme.spacing(0.5),
marginLeft: theme.spacing(0.5)
},
"&:first-child": {
borderRadius: 24,
marginLeft: theme.spacing(0.25)
},
"&:last-child": {
borderRadius: 24,
marginRight: theme.spacing(0.25)
}
},
root: {
backgroundColor: darken(
theme.palette.background.paper,
getThemeType() === "light" ? 0.05 : 0.25
),
borderRadius: 24,
content: "border-box"
}
}))(ToggleButtonGroup);
// const StyledToggleButtonGroup = withStyles(theme => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }))(ToggleButtonGroup);
const StyledToggle = withStyles({
root: {
backgroundColor: "transparent",
overflow: "visible",
content: "content-box",
"&$selected": {
backgroundColor: "gold",
color: "black",
borderRadius: 24,
fontWeight: "bold"
},
"&$selected:hover": {
backgroundColor: "rgb(255, 255, 0)",
color: "black",
borderRadius: 24
}
},
selected: {}
})(ToggleButton);
// const StyledToggle = withStyles({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// })(ToggleButton);
const gradients = () => {
const burning = "#8a1f0d";
@ -832,7 +833,6 @@ export default function BinSVGV2(props: Props) {
cableGrainEndY = cableGrainStartY;
}
if (cableEstimate && cables.length > 0 && cableGrainPathPoints.length === cables.length) {
console.log("calc cable path");
let cablePath = cableGrainEstimate(
cableGrainPathPoints,
spacingX,
@ -1048,10 +1048,26 @@ export default function BinSVGV2(props: Props) {
<React.Fragment>
{showTempHum && (
<Box
display="flex"
alignContent="center"
justifyContent="center"
textAlign="center"
//visibility={showTempHum ? "visible" : "hidden"}
className={!isFullScreen ? classes.smallToggle : classes.fullToggle}>
<StyledToggleButtonGroup
<ButtonGroup
buttons={[
{
title: isFullScreen ? "Temperature" : "Temp",
function: () => setExtraDetails("temps")
},
{
title: grainType ? (isFullScreen ? "Moisture" : "EMC") : isFullScreen ? "Humidity" : "Hum",
function: () => setExtraDetails("hums")
}
]}
toggle
/>
{/* <StyledToggleButtonGroup
value={extraDetails}
exclusive
size="small"
@ -1068,7 +1084,7 @@ export default function BinSVGV2(props: Props) {
aria-label="Node Humidity">
{grainType ? (isFullScreen ? "Moisture" : "EMC") : isFullScreen ? "Humidity" : "Hum"}
</StyledToggle>
</StyledToggleButtonGroup>
</StyledToggleButtonGroup> */}
</Box>
)}
<Box

View file

@ -401,7 +401,7 @@ export default function BinStorageConditions(props: Props) {
alignContent="center"
alignItems="center"
justifyContent="space-between">
<Grid size={{ xs: 6 }}>
<Grid size={{ xs: 8 }}>
<Box display="flex" alignContent="center" alignItems="center">
<TemperatureIcon />
<Typography noWrap style={{ fontWeight: 650 }}>

View file

@ -71,6 +71,7 @@ import { ExtractMoisture } from "grain";
import SearchSelect, { Option } from "common/SearchSelect";
import Edit from "@mui/icons-material/Edit";
import { makeStyles, styled } from "@mui/styles";
import ButtonGroup from "common/ButtonGroup";
const useStyles = makeStyles((theme: Theme) => {
return ({
@ -280,54 +281,54 @@ export default function BinVisualizer(props: Props) {
const [storageDate, setStorageDate] = useState(moment().format("YYYY-MM-DD"));
const [storageTime, setStorageTime] = useState(moment().format("HH:mm"));
const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
root: {
backgroundColor: "transparent",
overflow: "visible",
content: "content-box",
"&$selected": {
backgroundColor: "gold",
color: "black",
borderRadius: 24,
fontWeight: "bold"
},
"&$selected:hover": {
backgroundColor: "rgb(255, 255, 0)",
color: "black",
borderRadius: 24
}
},
selected: {}
}));
// const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// }));
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
grouped: {
margin: theme.spacing(-0.5),
border: "none",
padding: theme.spacing(1),
"&:not(:first-child):not(:last-child)": {
borderRadius: 24,
marginRight: theme.spacing(0.5),
marginLeft: theme.spacing(0.5)
},
"&:first-child": {
borderRadius: 24,
marginLeft: theme.spacing(0.25)
},
"&:last-child": {
borderRadius: 24,
marginRight: theme.spacing(0.25)
}
},
root: {
backgroundColor: darken(
theme.palette.background.paper,
getThemeType() === "light" ? 0.05 : 0.25
),
borderRadius: 24,
content: "border-box"
}
}));
// const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }));
useEffect(() => {
setModeTime(moment(bin.status.lastModeChange));
@ -1705,6 +1706,18 @@ export default function BinVisualizer(props: Props) {
);
};
const determineToggle = (binMode: pond.BinMode) => {
switch (binMode) {
case pond.BinMode.BIN_MODE_STORAGE:
return [0]
case pond.BinMode.BIN_MODE_COOLDOWN:
return [1]
case pond.BinMode.BIN_MODE_DRYING:
case pond.BinMode.BIN_MODE_HYDRATING:
return [2]
}
}
const binMode = () => {
const mode = bin.settings.mode;
return (
@ -1720,7 +1733,31 @@ export default function BinVisualizer(props: Props) {
<Typography variant="subtitle1" style={{ fontWeight: 800 }}>
Bin Mode
</Typography>
<StyledToggleButtonGroup
<ButtonGroup
buttons={[
{
title: "Storage",
function: () => {
setModeStorage()
}
},
{
title: "Cooldown",
function: () => {
setModeCooldown()
}
},
{
title: bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? "Hydrating" : "Drying",
function: () => {
setShowInputMoisture(true)
}
}
]}
toggledButtons={determineToggle(mode)}
toggle
/>
{/* <StyledToggleButtonGroup
id="tour-bin-mode"
value={mode}
exclusive
@ -1754,7 +1791,7 @@ export default function BinVisualizer(props: Props) {
Drying
</StyledToggle>
)}
</StyledToggleButtonGroup>
</StyledToggleButtonGroup> */}
</Box>
);
};
@ -2067,6 +2104,7 @@ export default function BinVisualizer(props: Props) {
devices={devices}
refreshCallback={success => {
setShowInputMoisture(false);
setNewPreset(0)
if (success) updateBin();
}}
parentPreset={newPreset}

View file

@ -130,6 +130,7 @@ const useStyles = makeStyles((theme: Theme) => {
borderTopLeftRadius: "6px",
borderTopRightRadius: "6px",
background: theme.palette.background.default,
fontWeight: 1000,
transform: "0.3s ease-in-out",
// "linear-gradient(rgba(150, 150, 150, 0.3),"
// + theme.palette.background.default + " 80%)"

View file

@ -123,10 +123,11 @@ export default function BinyardDisplay(props: Props) {
});
const loadBins = useCallback(() => {
setBinsLoading(true);
if (yardKey === "") return
//load the bins based on the yard key
setBinsLoading(true);
binAPI
.listBinsAndData(maxBins, 0, "asc", "name", undefined, as, undefined, [yardKey], ["binyard"])
.listBinsAndData(maxBins, 0, "asc", "name", undefined, undefined, undefined, [yardKey], ["binyard"])
.then(resp => {
let yardBins: Bin[] = [];
let grainBins: Bin[] = [];
@ -315,17 +316,10 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
valDisplay="average"
bins={grainBins}
duplicateBin={duplicateBin}
title={"Grain Bins"}
// because this only triggers in scroll view it is no longer necessary as we dont do the scroll here
// loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) {
// loadMoreBins(contentFilter);
// setScrollTranslations({ ...scrollTranslations, bins: newTranslation });
// }
// }}
/>
</Grid>
</React.Fragment>
@ -338,16 +332,10 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
valDisplay="average"
bins={fertBins}
duplicateBin={duplicateBin}
title={"Fertilizer Bins"}
// loadMore={newTranslation => {
// //only triggered in the scroll view so this will never trigger in grid view
// if (yardBins.length < binTotal) {
// loadMoreBins(contentFilter);
// setScrollTranslations({ ...scrollTranslations, fertilizer: newTranslation });
// }
// }}
/>
</Grid>
</React.Fragment>
@ -360,6 +348,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
valDisplay="average"
bins={emptyBins}
duplicateBin={duplicateBin}
title={"Empty Bins"}

161
src/common/ButtonGroup.tsx Normal file
View file

@ -0,0 +1,161 @@
import { Box, Button, darken, Theme, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import { cloneDeep } from "lodash";
import { useEffect } from "react";
import { useState } from "react";
import { getSecondaryColour } from "services/whiteLabel";
import { getThemeType } from "theme/themeType";
export interface ButtonData {
/**
* The title and description of the button
*/
title: string
/**
* An icon that would replace the text of the button
*/
icon?: JSX.Element
/**
* The function to run when the button is clicked or toggled on when toggle is true
*/
function: () => void
}
interface Props {
/**
* The array of button data to be rendered by the group
*/
buttons: ButtonData[]
/**
* When true will track the toggle state of the buttons, false will just be basic buttons
*
* @default false
*/
toggle?: boolean
/**
* When true will allow multiple buttons to be toggled, false will untoggle the previous button when another is selected
*
* @default false
*/
multiToggle?: boolean
/**
* Can be used to control which buttons are toggled on from the parent by passing the indexes of the buttons to toggle on.
* If not passed in will default to toggle the first button and then be controlled internally
*
* @default undefined
*/
toggledButtons?: number[]
/**
* Numerical value for the size of the text for buttons that dont use the icon
*/
textSize?: number
}
const useStyles = makeStyles((theme: Theme) => {
return ({
container: {
backgroundColor: darken(
theme.palette.background.paper,
getThemeType() === "light" ? 0.10 : 0.05
),
borderRadius: 24,
content: "border-box",
border: "none",
//padding: theme.spacing(1),
},
button: {
backgroundColor: "transparent",
color: theme.palette.text.primary,
overflow: "visible",
content: "content-box",
borderRadius: 24,
marginLeft: theme.spacing(0.05),
marginRight: theme.spacing(0.05),
"&:hover": {
//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", //for now Dustin just wants it to be gold
color: "black",
borderRadius: 24,
fontWeight: "bold"
}
})
})
export default function ButtonGroup(props: Props){
const { buttons, toggle, toggledButtons, multiToggle, textSize } = props
const classes = useStyles()
const [currentToggle, setCurrentToggle] = useState<number[]>([])
useEffect(()=>{
//determine the default state of the button toggles if toggle is true
if(toggle){
let toggled: number [] = [0]
if(toggledButtons){
toggled = toggledButtons
}
setCurrentToggle(toggled)
}
},[toggle, toggledButtons])
const checkToggled = (index: number) => {
return currentToggle.includes(index)
}
const toggleControl = (i: number) => {
let toggled = cloneDeep(currentToggle)
//is the button currently toggled
if(checkToggled(i)){
//if the button is toggled
if(multiToggle){
//un-toggle the button (remove it from the array)
console.log(toggled)
toggled.splice(currentToggle.indexOf(i), 1)
}
//and if it is single toggle do nothing
} else {
//if multiToggle is false
if(!multiToggle){
//replace the currentToggle array
toggled = [i]
}else{
//add it to the currentToggle array
toggled.push(i)
}
}
setCurrentToggle(toggled)
}
return (
<Box className={classes.container} display="flex" flexDirection="row">
{buttons.map((b, i) => (
<Button
key={i}
className={checkToggled(i) ? classes.buttonToggled : classes.button}
onClick={() => {
if(toggle){
//control the toggle internally if the array was not passed in
if(!toggledButtons){
toggleControl(i)
}
}
b.function()
}}>
{b.icon
?
b.icon
:
<Typography sx={{fontSize: textSize ?? 13, fontWeight: checkToggled(i) ? 650 : 500}}>
{b.title}
</Typography>
}
</Button>
))}
</Box>
)
}

View file

@ -133,7 +133,7 @@ export default function DisplayDrawer(props: Props) {
</Grid>
<Grid>
<Box>
<Typography align="center" variant="h5">
<Typography sx={{ color: "black", fontWeight: 650}} align="center" variant="h5">
{title}
</Typography>
</Box>

View file

@ -3,7 +3,7 @@ import { Beenhere } from "@mui/icons-material";
import { useThemeType } from "hooks";
import moment from "moment";
import React, { useEffect, useState } from "react";
import Map, { Marker } from "react-map-gl/mapbox-legacy";
import Map, { Marker } from "react-map-gl/mapbox";
const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

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

@ -2,7 +2,8 @@ import {
Button,
DialogActions,
DialogContent,
Grid,
Grid2 as Grid,
TextField,
Theme,
Typography
} from "@mui/material";
@ -13,6 +14,8 @@ import ResponsiveDialog from "common/ResponsiveDialog";
import { DateRangePreset, GetDefaultDateRange, SetDefaultPreset } from "./DateRange";
import { useThemeType } from "hooks";
import { makeStyles } from "@mui/styles";
import { cloneDeep } from "lodash";
import { color } from "@mui/system";
interface Props {
startDate: Moment;
@ -26,10 +29,12 @@ type DateRange<T> = [T | null, T | null];
const useStyles = makeStyles((_theme: Theme) => {
return ({
activeButtonLight: {
border: "1px solid black"
border: "1px solid black",
color: "black"
},
acitveButtonDark: {
border: "1px solid white"
border: "1px solid white",
color: "white"
}
})
});
@ -49,7 +54,7 @@ export default function TimeBar(props: Props) {
const submitDateRange = () => {
const startDate = dateRange[0] ? dateRange[0].clone() : moment();
const endDate = dateRange[1] ? dateRange[1].clone() : moment();
const endDate = dateRange[1] ? dateRange[1].add(1, "day").clone() : moment(); //add 1 day to the end time to include that day rather than have it be the cutoff
updateDateRange(startDate, endDate, false);
setDateRangeDialog(false);
setActiveView("selectRange");
@ -85,19 +90,30 @@ export default function TimeBar(props: Props) {
onClose={() => setDateRangeDialog(false)}
aria-labelledby="date-range-dialog">
<DialogContent>
{/* <StaticDateRangePicker
disableFuture
value={dateRange}
onChange={date => setDateRange(date)}
renderInput={(startProps, endProps) => (
<React.Fragment>
<TextField {...startProps} />
<DateRangeDelimiter> to </DateRangeDelimiter>
<TextField {...endProps} />
</React.Fragment>
)}
/> */}
DATE PICKER GOES HERE
<TextField
fullWidth
margin="normal"
type="date"
label="Start Time"
value={dateRange[0]?.format("YYYY-MM-DD")}
onChange={e => {
let range = cloneDeep(dateRange)
range[0] = moment(e.target.value)
setDateRange(range)
}}
/>
<TextField
fullWidth
margin="normal"
type="date"
label="End Time"
value={dateRange[1]?.format("YYYY-MM-DD")}
onChange={e => {
let range = cloneDeep(dateRange)
range[1] = moment(e.target.value)
setDateRange(range)
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setDateRangeDialog(false)} color="primary">
@ -115,7 +131,7 @@ export default function TimeBar(props: Props) {
<React.Fragment>
{datePickerDialog()}
<Grid container direction="row" spacing={1} wrap="nowrap">
<Grid item>
<Grid>
<Button
style={{ borderRadius: "25px" }}
className={
@ -132,7 +148,7 @@ export default function TimeBar(props: Props) {
<Typography style={{ fontWeight: 650 }}>1H</Typography>
</Button>
</Grid>
<Grid item>
<Grid>
<Button
style={{ borderRadius: "25px" }}
className={
@ -149,7 +165,7 @@ export default function TimeBar(props: Props) {
<Typography style={{ fontWeight: 650 }}>1D</Typography>
</Button>
</Grid>
<Grid item>
<Grid>
<Button
style={{ borderRadius: "25px" }}
className={
@ -166,7 +182,7 @@ export default function TimeBar(props: Props) {
<Typography style={{ fontWeight: 650 }}>1W</Typography>
</Button>
</Grid>
<Grid item>
<Grid>
<Button
style={{ borderRadius: "25px" }}
className={
@ -183,7 +199,7 @@ export default function TimeBar(props: Props) {
<Typography style={{ fontWeight: 650 }}>1M</Typography>
</Button>
</Grid>
<Grid item>
<Grid>
<Button
style={{ borderRadius: "25px" }}
className={

View file

@ -114,7 +114,10 @@ export default function ComponentActions(props: Props) {
onClose={closeMoreMenu}
disableAutoFocusItem>
{canAddInteraction && (
<MenuItem dense onClick={() => setIsAddInteractionOpen(true)} divider>
<MenuItem dense onClick={() => {
setIsAddInteractionOpen(true)
closeMoreMenu()
}} divider>
<ListItemIcon>
<AddCircle className={classes.addIcon} />
</ListItemIcon>
@ -126,6 +129,7 @@ export default function ComponentActions(props: Props) {
onClick={() => {
setIsJSON(false);
setIsExportDataOpen(true);
closeMoreMenu()
}}>
<ListItemIcon>
<SaveAlt className={classes.exportDataIcon} />
@ -134,7 +138,10 @@ export default function ComponentActions(props: Props) {
</MenuItem>
{canEdit && <Divider />}
{canEdit && (
<MenuItem dense onClick={() => openComponentSettingsDialog("remove")}>
<MenuItem dense onClick={() => {
openComponentSettingsDialog("remove")
setAnchorEl(null);
}}>
<ListItemIcon>
<DeleteOutline className={classes.red} />
</ListItemIcon>

View file

@ -254,15 +254,15 @@ export default function ComponentCard(props: Props) {
size="small"
onTouchStart={event => {
event.stopPropagation();
event.preventDefault();
//event.preventDefault();
}}
onMouseDown={event => {
event.stopPropagation();
event.preventDefault();
//event.preventDefault();
}}
onClick={event => {
event.stopPropagation();
event.preventDefault();
//event.preventDefault();
}}
disabled={!canEdit}
onChange={(_, checked) => updateControllerState(checked)}

View file

@ -237,8 +237,7 @@ export default function ComponentForm(props: Props) {
};
useEffect(() => {
let comp = tidyComponent(form.component);
componentChanged(comp, isFormValid());
componentChanged(form.component, isFormValid());
}, [form.component, componentChanged]); // eslint-disable-line react-hooks/exhaustive-deps
const isNameValid = (name: string): boolean => {
@ -950,7 +949,7 @@ export default function ComponentForm(props: Props) {
<FormControlLabel
control={
<Switch
checked={measure}
checked={measure}
onChange={toggleMeasure}
name="measure"
aria-label="measure"

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";
@ -70,6 +71,8 @@ import SaveDeviceProfile from "./SaveDeviceProfile";
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()
@ -168,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;
@ -494,10 +510,19 @@ export default function DeviceActions(props: Props) {
);
};
const showSupport = () => {
return user.allowedTo("provision") && !location.pathname.includes("support")
}
const buttons = () => {
return (
<React.Fragment>
{/* {canWrite && user.allowedTo("provision") && <UpgradeDevice device={device} />} */}
{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
notify={preferences.notify}

View file

@ -0,0 +1,321 @@
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
IconButton,
LinearProgress,
Link,
Theme,
Tooltip,
Typography
} from "@mui/material";
// 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 moment from "moment";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { useGlobalState } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { or } from "utils/types";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => {
return ({
dialogContent: {
marginTop: theme.spacing(2)
},
buttonProgress: {
color: theme.palette.primary.main,
position: "absolute",
zIndex: 1
},
failed: {
color: theme.palette.error.main
}
})
});
interface Props {
device: Device;
}
export default function UpgradeDevice(props: Props) {
const classes = useStyles();
const deviceAPI = useDeviceAPI();
const { success, error } = useSnackbar();
const firmwareAPI = useFirmwareAPI();
const [{ firmware }] = useGlobalState();
const prevFirmware = usePrevious(firmware);
const { device } = props;
const prevDevice = usePrevious(device);
const [open, setOpen] = useState<boolean>(false);
const prevOpen = usePrevious(open);
const [upgrade, setUpgrade] = useState<Upgrade>(Upgrade.create());
const [gettingStatus, setGettingStatus] = useState(0);
const progress = useCallback(() => {
return upgrade.status.progress;
}, [upgrade.status.progress]);
const currentFirmware = () => {
return device.status.firmwareVersion;
};
const latestFirmware = () => {
return latestFirmwareVersion(
firmware,
device.settings.platform,
device.settings.upgradeChannel
);
};
const upgradeVersion = () => {
return upgrade.settings.version;
};
const available = () => {
return currentFirmware() !== latestFirmware();
};
const upgrading = useCallback(() => {
return progress() === "upgrading";
}, [progress]);
const completed = () => {
return progress() === "completed" && currentFirmware() === latestFirmware();
};
const failed = () => {
return progress() === "failed" && upgradeVersion() === latestFirmware();
};
const percent = () => {
return Math.round((upgrade.status.offset / upgrade.status.size) * 100);
};
const tooltip = () => {
return upgrading()
? "Upgrade in progress: " + percent() + "%"
: failed()
? "Upgrade failed"
: "Upgrade Device";
};
const upgradeTimingDesc = () => {
let desc = "requested " + moment(or(upgrade.status.started, 0)).fromNow();
if (completed()) {
desc = "completed " + moment(or(upgrade.status.stopped, 0)).fromNow();
} else if (failed()) {
desc = "failed " + moment(or(upgrade.status.changed, 0)).fromNow();
}
return desc;
};
const mode = () => {
let mode = "prompt";
if (upgrading() || failed() || completed()) {
mode = "progress";
}
return mode;
};
const getStatus = useCallback(() => {
if (gettingStatus === device.id() && !upgrading()) {
return;
}
setGettingStatus(device.id());
deviceAPI
.getUpgradeStatus(device.id(), getContextKeys(), getContextTypes())
.then((res: any) => {
if (res && res.data && res.data.upgrade) {
setUpgrade(Upgrade.any(res.data.upgrade));
}
})
.catch((err: any) => error(err));
}, [device, deviceAPI, error, gettingStatus, upgrading]);
useEffect(() => {
const deviceIdUnchanged = !prevDevice || prevDevice.id() === device.id();
if (
(open && !prevOpen) ||
firmware !== prevFirmware ||
(device !== prevDevice && deviceIdUnchanged)
) {
getStatus();
}
}, [open, prevOpen, getStatus, device, prevDevice, firmware, prevFirmware]);
useEffect(() => {
function getStatusAgain() {
if (upgrading()) {
getStatus();
}
}
const interval = setInterval(() => getStatusAgain(), 5000);
return () => {
clearInterval(interval);
};
}, [open, getStatus, upgrading]);
const close = () => {
setOpen(false);
};
const handleUpgrade = () => {
firmwareAPI
.upgradeFirmware(device.id())
.then(() => {
success(device.name() + " will upgrade to " + latestFirmware() + " next time it checks in");
getStatus();
})
.catch((err: any) => {
console.error(err);
error(err);
});
};
const restart = () => {
firmwareAPI
.upgradeFirmware(device.id())
.then(() => {
success(
"Upgrade successfully restarted, " +
device.name() +
" will upgrade next time it checks in"
);
getStatus();
})
.catch((err: any) => {
console.error(err);
error(err);
});
};
const cancel = () => {
firmwareAPI
.cancelUpgrade(device.id())
.then(() => {
success("Upgrade cancelled");
})
.catch((err: any) => {
console.error(err);
error(err);
});
};
const button = () => {
return (
<Tooltip title={tooltip()}>
<IconButton
color="default"
className={failed() ? classes.failed : undefined}
onClick={() => setOpen(true)}
aria-label={tooltip()}>
<UpdateIcon />
{upgrading() && (
<CircularProgress
// variant="static"
value={percent()}
size={"2rem"}
className={classes.buttonProgress}
/>
)}
</IconButton>
</Tooltip>
);
};
return (
<React.Fragment>
{(available() || upgrading() || failed()) && button()}
<Dialog fullWidth open={open} onClose={close} aria-labelledby="form-dialog-title">
{mode() === "prompt" && (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Upgrade Device
<Typography variant="body2" color="textSecondary">
{device.name()}
</Typography>
</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
Upgrading from {currentFirmware()} to {latestFirmware()}
<br />
<br />
It is recommended that the device be plugged in during the upgrade as it will take
several minutes to complete
<br />
<br />
While the upgrade is in progress you can return to this dialog to view its status
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={close} color="primary">
Close
</Button>
<Button onClick={handleUpgrade} color="primary">
Upgrade
</Button>
</DialogActions>
</React.Fragment>
)}
{mode() === "progress" && (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Upgrading Device
<Typography variant="body2" color="textSecondary">
{device.name()}
</Typography>
</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
Upgrade to {latestFirmware()} {upgradeTimingDesc()}
</DialogContentText>
<br />
<LinearProgress variant="determinate" value={percent()} />
<DialogContentText>
{or(upgrade.status.offset, 0)} / {upgrade.status.size} bytes ({percent()}%)
</DialogContentText>
{failed() && (
<React.Fragment>
<DialogContentText>
<br />
Uh oh something went wrong. You can try to{" "}
<Link variant="body2" onClick={restart} style={{ cursor: "pointer" }}>
restart
</Link>{" "}
the upgrade, if the problem persists we'll be alerted and follow up as soon as
possible.
</DialogContentText>
</React.Fragment>
)}
</DialogContent>
<DialogActions>
<Button onClick={close} color="primary">
Close
</Button>
{upgrading() && (
<Button onClick={cancel} style={{ color: "red" }}>
Cancel
</Button>
)}
{failed() && (
<Button onClick={restart} color="primary">
Restart
</Button>
)}
</DialogActions>
</React.Fragment>
)}
</Dialog>
</React.Fragment>
);
}

View file

@ -22,6 +22,7 @@ import GateSVG from "./GateSVG";
import GateGraphs from "./GateGraphs";
//import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import { getThemeType } from "theme";
import ButtonGroup from "common/ButtonGroup";
interface Props {
gate: Gate;
@ -30,6 +31,8 @@ interface Props {
drawerView?: boolean;
}
export default function GateDevice(props: Props) {
const { gate, comprehensiveDevice, linkedCompList, drawerView } = props;
const [{as}] = useGlobalState();
@ -233,6 +236,7 @@ export default function GateDevice(props: Props) {
return (
<Select
id="ambientSelect"
variant="standard"
value={ambientKey}
style={{ fontSize: 8 }}
onChange={e => {
@ -265,6 +269,7 @@ export default function GateDevice(props: Props) {
return (
<Select
id="tempSelect"
variant="standard"
value={tempKey}
style={{ fontSize: 8 }}
onChange={e => {
@ -299,6 +304,7 @@ export default function GateDevice(props: Props) {
return (
<Select
id="pressureSelect"
variant="standard"
value={pressureKey}
style={{ fontSize: 8 }}
onChange={e => {
@ -334,9 +340,10 @@ export default function GateDevice(props: Props) {
container
direction="row"
alignContent="center"
width="100%"
//alignItems="center"
>
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 8}} style={{ padding: 10 }}>
<Grid size={{ sm: 12, lg: isMobile || drawerView ? 12 : 4}} style={{ padding: 10 }}>
<Box
paddingLeft={1}
display="flex"
@ -344,7 +351,20 @@ export default function GateDevice(props: Props) {
alignItems="center"
marginBottom={2}>
<Typography style={{ fontWeight: 650, fontSize: 20 }}>{device.name()}</Typography>
<ToggleButtonGroup value={detail} exclusive size="small" aria-label="detail">
<ButtonGroup
toggle
buttons={[
{
title: "Analysis",
function: () => setDetail("analytics")
},
{
title: "Sensors",
function: () => setDetail("sensors")
}
]}
/>
{/* <ToggleButtonGroup value={detail} exclusive size="small" aria-label="detail">
<ToggleButton
onClick={() => setDetail("analytics")}
value={"analytics"}
@ -357,7 +377,7 @@ export default function GateDevice(props: Props) {
aria-label="Sensor Graphs">
Sensors
</ToggleButton>
</ToggleButtonGroup>
</ToggleButtonGroup> */}
</Box>
<Card raised>
<GateSVG

View file

@ -355,13 +355,13 @@ export default function GateSVG(props: Props) {
{renderValues()}
</g>
<foreignObject height={20} width={120} x={10} y={83}>
<Box style={{ position: "fixed" }}>{ambientSelector()}</Box>
<Box sx={{ position: "fixed" }}>{ambientSelector()}</Box>
</foreignObject>
<foreignObject height={20} width={120} x={10} y={118}>
<Box style={{ position: "fixed" }}>{tempChainSelector()}</Box>
<Box sx={{ position: "fixed" }}>{tempChainSelector()}</Box>
</foreignObject>
<foreignObject height={20} width={120} x={10} y={153}>
<Box style={{ position: "fixed" }}>{pressureChainSelector()}</Box>
<Box sx={{ position: "fixed" }}>{pressureChainSelector()}</Box>
</foreignObject>
{pcaRed()}
{pcaGreen()}

View file

@ -33,7 +33,7 @@ import { GeometryMapping, shapeFromCoords } from "models/GeometryMapping";
import { FeatureCollection, Feature } from "geojson";
import DrawController from "./mapControllers/drawController";
//import { Geometry } from "geojson";
import Map, { MapRef, Marker, MarkerDragEvent } from "react-map-gl/mapbox-legacy";
import Map, { MapRef, Marker, MarkerDragEvent } from "react-map-gl/mapbox";
import { getDistanceUnit } from "utils";
import { MapMouseEvent } from "mapbox-gl";
import { makeStyles } from "@mui/styles";

View file

@ -1,4 +1,4 @@
import { ControlPosition, useMap } from "react-map-gl/mapbox-legacy";
import { ControlPosition, useMap } from "react-map-gl/mapbox";
import MapboxGeocoder, { Result } from "@mapbox/mapbox-gl-geocoder";
import { useEffect, useState } from "react";

View file

@ -1,5 +1,5 @@
import { useEffect, useRef } from "react";
import { useMap } from "react-map-gl/mapbox-legacy";
import { useMap } from "react-map-gl/mapbox";
import { FeatureCollection } from "geojson";
import MapboxDraw from "@mapbox/mapbox-gl-draw";

View file

@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { Layer, Source } from "react-map-gl/mapbox-legacy";
import { Layer, Source } from "react-map-gl/mapbox";
import { FeatureCollection } from "geojson";
interface Props {

View file

@ -5,7 +5,7 @@ import { useMobile } from "hooks";
import { clone } from "lodash";
import { pond } from "protobuf-ts/pond";
import React, { useCallback, useEffect, useState } from "react";
import { Marker } from "react-map-gl/mapbox-legacy";
import { Marker } from "react-map-gl/mapbox";
//interface for markers
export interface MarkerData {

View file

@ -703,7 +703,12 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
</Grid>
</Grid>
<Grid size={3}>
<Button style={{ margin: "auto" }} onClick={() => setOpenFMSettings(true)}>
<Button
style={{ margin: "auto" }}
onClick={() => {
setOpenFMSettings(true)
setAnchorFieldMarker(null)
}}>
<Edit />
</Button>
</Grid>

32
src/models/Upgrade.ts Normal file
View file

@ -0,0 +1,32 @@
import { pond } from "protobuf-ts/pond";
import { or } from "utils/types";
import { cloneDeep } from "lodash";
export class Upgrade {
public settings: pond.UpgradeSettings = pond.UpgradeSettings.create();
public status: pond.UpgradeStatus = pond.UpgradeStatus.create();
public static create(pb?: pond.Upgrade): Upgrade {
let my = new Upgrade();
if (pb) {
my.settings = pond.UpgradeSettings.fromObject(or(pb.settings, {}));
my.status = pond.UpgradeStatus.fromObject(or(pb.status, {}));
}
return my;
}
public static any(data: any): Upgrade {
return Upgrade.create(pond.Upgrade.fromObject(cloneDeep(data)));
}
public static clone(other: Upgrade): Upgrade {
let my = new Upgrade();
my.settings = pond.UpgradeSettings.fromObject(cloneDeep(other.settings));
my.status = pond.UpgradeStatus.fromObject(cloneDeep(other.status));
return my;
}
public key(): string {
return this.settings.key;
}
}

View file

@ -11,7 +11,7 @@ export * from "./ShareableLink";
export * from "./Site";
export * from "./Tag";
export * from "./Task";
// export * from "./Upgrade";
export * from "./Upgrade";
// export * from "./User";
export * from "./user";
export * from "./Field";

View file

@ -105,7 +105,7 @@ export default function BottomNavigator(props: Props) {
return (
<BottomNavigation value={route} onChange={(_, newValue) => handleRouteChange(newValue)}>
{isAdaptive && (
<BottomNavigationAction label="Farm" icon={<FieldsIcon type={getType()} />} value="fields" />
<BottomNavigationAction label="Farm" icon={<FieldsIcon type={getType()} />} value="visualFarm" />
)}
{isAdaptive && (
<BottomNavigationAction label="Bins" icon={<BinsIcon type={getType()} />} value="bins" />
@ -114,7 +114,7 @@ export default function BottomNavigator(props: Props) {
<BottomNavigationAction
label="Site Map"
icon={<FieldsIcon type={getType()} />}
value="constructionsiteMap"
value="constructionMap"
/>
)}
{isOmni && (

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

@ -160,84 +160,6 @@ export default function SideNavigator(props: Props) {
const isAdCon = IsAdCon()
return (
<List className={classes.list} component="nav">
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Bins" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/bins")}
classes={getClasses("/bin")}
>
<ListItemIcon>
<BinsIcon height={"26px"}/>
</ListItemIcon>
{open && <ListItemText primary="Bins" />}
</ListItemButton>
</Tooltip>
)}
{(isMiPCA || user.hasFeature("admin")) && (
<Tooltip title="Terminals" placement="right">
<ListItemButton
id="tour-terminals"
onClick={() => goTo("/terminals")}
classes={getClasses("/terminal")}
>
<ListItemIcon>
<PlaneIcon />
</ListItemIcon>
{open && <ListItemText primary="Terminals" />}
</ListItemButton>
</Tooltip>
)}
<Tooltip title="Devices" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/devices")}
classes={getClasses("/device")}
>
<ListItemIcon>
<BindaptIcon />
</ListItemIcon>
{open && <ListItemText primary="Devices" />}
</ListItemButton>
</Tooltip>
{(isMiVent || user.hasFeature("admin")) && (
<Tooltip title="Mines" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/mines")}
classes={getClasses("/mine")}
>
<ListItemIcon>
<MiningIcon height={"26px"} width={"26px"} />
</ListItemIcon>
{open && <ListItemText primary="Mines" />}
</ListItemButton>
</Tooltip>
)}
<Tooltip title="Teams" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/teams")}
classes={getClasses("/team")}
>
<ListItemIcon>
<People style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
</ListItemIcon>
{open && <ListItemText primary="Teams" />}
</ListItemButton>
</Tooltip>
<Tooltip title="Users" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/users")}
classes={getClasses("/user")}
>
<ListItemIcon>
<Person style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
</ListItemIcon>
{open && <ListItemText primary="Users" />}
</ListItemButton>
</Tooltip>
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Visual Farm" placement="right">
<ListItemButton
@ -252,20 +174,6 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
)}
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="My Fields" placement="right">
<ListItemButton
id="tour-field-list"
onClick={() => goTo("/fields")}
classes={getClasses("/fields")}
>
<ListItemIcon>
<FieldListIcon />
</ListItemIcon>
{open && <ListItemText primary="My Fields" />}
</ListItemButton>
</Tooltip>
)}
{(isMiPCA || user.hasFeature("admin")) && (
<Tooltip title="Aviation Map" placement="right">
<ListItemButton
@ -294,6 +202,169 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
)}
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Contracts" placement="right">
<ListItemButton
id="tour-contracts"
onClick={() => goTo("/contracts")}
classes={getClasses("/contracts")}
>
<ListItemIcon>
<ContractsIcon />
</ListItemIcon>
{open && <ListItemText primary="Contracts" />}
</ListItemButton>
</Tooltip>
)}
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Bins" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/bins")}
classes={getClasses("/bin")}
>
<ListItemIcon>
<BinsIcon height={"26px"}/>
</ListItemIcon>
{open && <ListItemText primary="Bins" />}
</ListItemButton>
</Tooltip>
)}
{(isMiPCA || user.hasFeature("admin")) && (
<Tooltip title="Terminals" placement="right">
<ListItemButton
id="tour-terminals"
onClick={() => goTo("/terminals")}
classes={getClasses("/terminal")}
>
<ListItemIcon>
<PlaneIcon />
</ListItemIcon>
{open && <ListItemText primary="Terminals" />}
</ListItemButton>
</Tooltip>
)}
{(user.hasFeature("installer") && isAg || user.hasFeature("admin")) &&
<Tooltip title="Cable Estimator" placement="right">
<ListItemButton
id="tour-cable-estimator"
onClick={() => goTo("/cableEstimate")}
classes={getClasses("/cableEstimate")}
>
<ListItemIcon>
<CableIcon />
</ListItemIcon>
{open && <ListItemText primary="Cable Estimator" />}
</ListItemButton>
</Tooltip>
}
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Transactions" placement="right">
<ListItemButton
id="tour-transactions"
onClick={() => goTo("/transactions")}
classes={getClasses("/transactions")}
>
<ListItemIcon>
<SyncAlt />
</ListItemIcon>
{open && <ListItemText primary="Transactions" />}
</ListItemButton>
</Tooltip>
)}
{(isMiVent || user.hasFeature("admin")) && (
<Tooltip title="Mines" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/mines")}
classes={getClasses("/mine")}
>
<ListItemIcon>
<MiningIcon height={"26px"} width={"26px"} />
</ListItemIcon>
{open && <ListItemText primary="Mines" />}
</ListItemButton>
</Tooltip>
)}
<Tooltip title="Devices" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/devices")}
classes={getClasses("/device")}
>
<ListItemIcon>
<BindaptIcon />
</ListItemIcon>
{open && <ListItemText primary="Devices" />}
</ListItemButton>
</Tooltip>
{user.hasFeature("admin") &&
<Tooltip title="Firmware" placement="right">
<ListItemButton
id="tour-firmware"
onClick={() => goTo("/firmware")}
classes={getClasses("/firmware")}
>
<ListItemIcon>
<Memory />
</ListItemIcon>
{open && <ListItemText primary="Logs" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") &&
<Tooltip title="Logs" placement="right">
<ListItemButton
id="tour-logs"
onClick={() => goTo("/logs")}
classes={getClasses("/logs")}
>
<ListItemIcon>
<DataDuckIcon />
</ListItemIcon>
{open && <ListItemText primary="Logs" />}
</ListItemButton>
</Tooltip>
}
<Tooltip title="Teams" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/teams")}
classes={getClasses("/team")}
>
<ListItemIcon>
<People style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
</ListItemIcon>
{open && <ListItemText primary="Teams" />}
</ListItemButton>
</Tooltip>
<Tooltip title="Users" placement="right">
<ListItemButton
id="tour-dashboard"
onClick={() => goTo("/users")}
classes={getClasses("/user")}
>
<ListItemIcon>
<Person style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
</ListItemIcon>
{open && <ListItemText primary="Users" />}
</ListItemButton>
</Tooltip>
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="My Fields" placement="right">
<ListItemButton
id="tour-field-list"
onClick={() => goTo("/fields")}
classes={getClasses("/fields")}
>
<ListItemIcon>
<FieldListIcon />
</ListItemIcon>
{open && <ListItemText primary="My Fields" />}
</ListItemButton>
</Tooltip>
)}
{(isAdCon || user.hasFeature("admin")) && (
<Tooltip title="Jobsites" placement="right">
<ListItemButton
@ -334,48 +405,7 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Tasks" />}
</ListItemButton>
</Tooltip>
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Transactions" placement="right">
<ListItemButton
id="tour-transactions"
onClick={() => goTo("/transactions")}
classes={getClasses("/transactions")}
>
<ListItemIcon>
<SyncAlt />
</ListItemIcon>
{open && <ListItemText primary="Transactions" />}
</ListItemButton>
</Tooltip>
)}
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Contracts" placement="right">
<ListItemButton
id="tour-contracts"
onClick={() => goTo("/contracts")}
classes={getClasses("/contracts")}
>
<ListItemIcon>
<ContractsIcon />
</ListItemIcon>
{open && <ListItemText primary="Contracts" />}
</ListItemButton>
</Tooltip>
)}
{(user.hasFeature("installer") && isAg || user.hasFeature("admin")) &&
<Tooltip title="Cable Estimator" placement="right">
<ListItemButton
id="tour-cable-estimator"
onClick={() => goTo("/cableEstimate")}
classes={getClasses("/cableEstimate")}
>
<ListItemIcon>
<CableIcon />
</ListItemIcon>
{open && <ListItemText primary="Cable Estimator" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("developer") &&
<Tooltip title="API documentation" placement="right">
<ListItemButton
@ -390,34 +420,6 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") &&
<Tooltip title="Firmware" placement="right">
<ListItemButton
id="tour-firmware"
onClick={() => goTo("/firmware")}
classes={getClasses("/firmware")}
>
<ListItemIcon>
<Memory />
</ListItemIcon>
{open && <ListItemText primary="Logs" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") &&
<Tooltip title="Logs" placement="right">
<ListItemButton
id="tour-logs"
onClick={() => goTo("/logs")}
classes={getClasses("/logs")}
>
<ListItemIcon>
<DataDuckIcon />
</ListItemIcon>
{open && <ListItemText primary="Logs" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") && window.NDEFReader &&
<Tooltip title="NFC" placement="right">
<ListItemButton

View file

@ -66,6 +66,7 @@ import { makeStyles, styled } from "@mui/styles";
import { useNavigate, useParams } from "react-router-dom";
import { Controller } from "models/Controller";
import TaskViewer from "tasks/TaskViewer";
import ButtonGroup from "common/ButtonGroup";
interface TabPanelProps {
children?: React.ReactNode;
@ -214,54 +215,54 @@ export default function Bin(props: Props) {
setValue(newValue);
};
const StyledToggle = styled(ToggleButton)(() => ({
root: {
backgroundColor: "transparent",
overflow: "visible",
content: "content-box",
"&$selected": {
backgroundColor: "gold",
color: "black",
borderRadius: 24,
fontWeight: "bold"
},
"&$selected:hover": {
backgroundColor: "rgb(255, 255, 0)",
color: "black",
borderRadius: 24
}
},
selected: {}
}))
// const StyledToggle = styled(ToggleButton)(() => ({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// }))
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
grouped: {
margin: theme.spacing(-0.5),
border: "none",
padding: theme.spacing(1),
"&:not(:first-child):not(:last-child)": {
borderRadius: 24,
marginRight: theme.spacing(0.5),
marginLeft: theme.spacing(0.5)
},
"&:first-child": {
borderRadius: 24,
marginLeft: theme.spacing(0.25)
},
"&:last-child": {
borderRadius: 24,
marginRight: theme.spacing(0.25)
}
},
root: {
backgroundColor: darken(
theme.palette.background.paper,
getThemeType() === "light" ? 0.05 : 0.25
),
borderRadius: 24,
content: "border-box"
}
}))
// const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
// grouped: {
// margin: theme.spacing(-0.5),
// border: "none",
// padding: theme.spacing(1),
// "&:not(:first-child):not(:last-child)": {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5)
// },
// "&:first-child": {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25)
// },
// "&:last-child": {
// borderRadius: 24,
// marginRight: theme.spacing(0.25)
// }
// },
// root: {
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === "light" ? 0.05 : 0.25
// ),
// borderRadius: 24,
// content: "border-box"
// }
// }))
const load = useCallback(() => {
if (loadRef.current || user.id() === "") return;
@ -547,7 +548,6 @@ export default function Bin(props: Props) {
};
const tasks = () => {
console.log(showTasks())
if (showTasks()) {
return (
<React.Fragment>
@ -816,9 +816,9 @@ export default function Bin(props: Props) {
const desktopView = () => {
return (
<Card style={{ padding: 15 }}>
<Card sx={{ padding: 1 }}>
<Grid container spacing={2} direction={"row"} justifyContent="center" alignItems="stretch">
<Grid size={{ xs: 12, sm: 12, md: 6, lg: 3 }}>
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 4 }}>
<Box
paddingX={1}
marginBottom={1}
@ -844,7 +844,7 @@ export default function Bin(props: Props) {
{overview()}
{preferences && binComponents(preferences)}
</Grid>
<Grid size={{ xs: 12, sm: 12, md: 6, lg: 3 }}>
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && (
<BinConditioningCard
@ -862,9 +862,34 @@ export default function Bin(props: Props) {
<BinStorageConditions bin={bin} headspaceCO2={headspaceCO2} cables={grainCables} />
</Box>
</Grid>
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 6 }}>
<Box paddingBottom={2}>
<StyledToggleButtonGroup
<Grid id="tour-graphs" size={{ xs: 12, sm: 12, md: 12, lg: 5.3 }}>
<Box paddingBottom={2} display="flex">
<ButtonGroup
toggle
buttons={[
{
title: "Inventory",
function: () => setDetail("inventory")
},
{
title: "Sensors",
function: () => setDetail("sensors")
},
{
title: "Analysis",
function: () => setDetail("analytics")
},
{
title: "Alerts",
function: () => setDetail("alerts")
},
{
title: "Presets",
function: () => setDetail("presets")
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={detail}
exclusive
@ -900,7 +925,7 @@ export default function Bin(props: Props) {
aria-label="Bin Mode Presets">
Presets
</StyledToggle>
</StyledToggleButtonGroup>
</StyledToggleButtonGroup> */}
</Box>
{detail === "alerts" && (
<Box>
@ -1089,11 +1114,12 @@ export default function Bin(props: Props) {
};
return (
<PageContainer>
<PageContainer padding={1}>
{showPlenumError()}
<Box
paddingBottom={isMobile || displayMobile ? 1 : 2}
paddingX={isMobile || displayMobile ? 1 : 2}>
paddingX={1}
>
{tabs()}
{isMobile && (
<Box

View file

@ -81,6 +81,7 @@ import { getThemeType } from "theme/themeType";
import GrainBagSettings from "grainBag/grainBagSettings";
import GrainBagList from "grainBag/grainBagList";
import BinYards from "bin/BinYards";
import ButtonGroup from "common/ButtonGroup";
// import { useHistory } from "react-router";
const useStyles = makeStyles((theme: Theme) => {
@ -200,56 +201,56 @@ export default function Bins(props: Props) {
// fertilizer: 0
// });
const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
'& .MuiToggleButtonGroup-grouped': {
// border: 'none' replaces the old 'grouped' border: "none"
border: 'none',
padding: theme.spacing(1),
// Middle buttons
'&:not(:first-child):not(:last-child)': {
borderRadius: 24,
marginRight: theme.spacing(0.5),
marginLeft: theme.spacing(0.5),
},
// First button
'&:first-child': {
borderRadius: 24,
marginLeft: theme.spacing(0.25),
},
// Last button
'&:last-child': {
borderRadius: 24,
marginRight: theme.spacing(0.25),
},
},
// Root styles
backgroundColor: darken(
theme.palette.background.paper,
getThemeType() === 'light' ? 0.05 : 0.25
),
borderRadius: 24,
boxSizing: 'content-box', // Fixed 'content: "border-box"' typo from original
}));
// const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({
// '& .MuiToggleButtonGroup-grouped': {
// // border: 'none' replaces the old 'grouped' border: "none"
// border: 'none',
// padding: theme.spacing(1),
// // Middle buttons
// '&:not(:first-child):not(:last-child)': {
// borderRadius: 24,
// marginRight: theme.spacing(0.5),
// marginLeft: theme.spacing(0.5),
// },
// // First button
// '&:first-child': {
// borderRadius: 24,
// marginLeft: theme.spacing(0.25),
// },
// // Last button
// '&:last-child': {
// borderRadius: 24,
// marginRight: theme.spacing(0.25),
// },
// },
// // Root styles
// backgroundColor: darken(
// theme.palette.background.paper,
// getThemeType() === 'light' ? 0.05 : 0.25
// ),
// borderRadius: 24,
// boxSizing: 'content-box', // Fixed 'content: "border-box"' typo from original
// }));
const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
root: {
backgroundColor: "transparent",
overflow: "visible",
content: "content-box",
"&$selected": {
backgroundColor: "gold",
color: "black",
borderRadius: 24,
fontWeight: "bold"
},
"&$selected:hover": {
backgroundColor: "rgb(255, 255, 0)",
color: "black",
borderRadius: 24
}
},
selected: {}
}));
// const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({
// root: {
// backgroundColor: "transparent",
// overflow: "visible",
// content: "content-box",
// "&$selected": {
// backgroundColor: "gold",
// color: "black",
// borderRadius: 24,
// fontWeight: "bold"
// },
// "&$selected:hover": {
// backgroundColor: "rgb(255, 255, 0)",
// color: "black",
// borderRadius: 24
// }
// },
// selected: {}
// }));
useEffect(() => {
let ebt = sessionStorage.getItem("expandBinTotal");
@ -502,6 +503,17 @@ export default function Bins(props: Props) {
}
}, [loadBins, props.insert, contentFilter]);
const determineNodeToggle = () => {
switch(cardValDisplay) {
case "low":
return [0]
case "high":
return [2]
default:
return [1]
}
}
const duplicateBin = (bin: Bin) => {
binAPI.addBin(bin.settings, as).then(resp => {
loadBins();
@ -1154,7 +1166,25 @@ export default function Bins(props: Props) {
Load All
</Button>
)}
<StyledToggleButtonGroup
<ButtonGroup
toggle
toggledButtons={determineNodeToggle()}
buttons={[
{
title: "Low",
function: () => {setCardValDisplay("low")}
},
{
title: "Average",
function: () => {setCardValDisplay("average")}
},
{
title: "High",
function: () => {setCardValDisplay("high")}
}
]}
/>
{/* <StyledToggleButtonGroup
id="cardValueDisplay"
value={cardValDisplay}
exclusive
@ -1184,12 +1214,33 @@ export default function Bins(props: Props) {
}}>
High
</StyledToggle>
</StyledToggleButtonGroup>
</StyledToggleButtonGroup> */}
</Box>
</Grid>
<Grid >
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<StyledToggleButtonGroup
<ButtonGroup
toggle
buttons={[
{
title: "Grid",
icon: <ViewComfy />,
function: () => {
setBinView("grid");
sessionStorage.setItem("binsView", "grid");
}
},
{
title: "List",
icon: <ViewList />,
function: () => {
setBinView("list");
sessionStorage.setItem("binsView", "list");
}
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={binView}
exclusive
@ -1203,7 +1254,7 @@ export default function Bins(props: Props) {
sessionStorage.setItem("binsView", "grid");
}}>
<ViewComfy />
</StyledToggle>
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
@ -1214,7 +1265,7 @@ export default function Bins(props: Props) {
}}>
<ViewColumn />
</StyledToggle> */}
<StyledToggle
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
@ -1223,7 +1274,7 @@ export default function Bins(props: Props) {
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup>
</StyledToggleButtonGroup> */}
<MoreVert
className={classes.icon}
onClick={event => {
@ -1261,7 +1312,28 @@ export default function Bins(props: Props) {
</Grid>
<Grid >
<Box display="flex" paddingY={1} alignContent="center" alignItems="center">
<StyledToggleButtonGroup
<ButtonGroup
toggle
buttons={[
{
title: "Grid",
icon: <ViewComfy />,
function: () => {
setBagView("grid");
sessionStorage.setItem("bagsView", "grid");
}
},
{
title: "List",
icon: <ViewList />,
function: () => {
setBagView("list");
sessionStorage.setItem("bagsView", "list");
}
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={bagView}
exclusive
@ -1275,7 +1347,7 @@ export default function Bins(props: Props) {
sessionStorage.setItem("bagsView", "grid");
}}>
<ViewComfy />
</StyledToggle>
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
@ -1286,7 +1358,7 @@ export default function Bins(props: Props) {
}}>
<ViewColumn />
</StyledToggle> */}
<StyledToggle
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
@ -1295,7 +1367,7 @@ export default function Bins(props: Props) {
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup>
</StyledToggleButtonGroup> */}
<IconButton
className={classes.icon}
onClick={() => {

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

@ -57,7 +57,14 @@ const useStyles = makeStyles((theme: Theme) => {
margin: theme.spacing(1),
marginLeft: theme.spacing(2),
display: "flex",
width: theme.spacing(26)
width: theme.spacing(32)
// justifyContent: "center",
},
tagsCellContainer: {
margin: theme.spacing(1),
marginLeft: theme.spacing(2),
display: "flex",
width: theme.spacing(24)
// justifyContent: "center",
},
green: {
@ -328,8 +335,8 @@ export default function Devices() {
<Box className={classes.descriptionCellContainer}>
<Tooltip title={device.settings?.description}>
<span>
{description.length > 32
? description.substring(0, 32) + "..."
{description.length > 36
? description.substring(0, 36) + "..."
: description}
</span>
</Tooltip>
@ -341,7 +348,7 @@ export default function Devices() {
title: "Tags",
render: (device: Device) => {
return (
<Grid2 container spacing={1} className={classes.descriptionCellContainer}>
<Grid2 container spacing={1} className={classes.tagsCellContainer}>
{device.status.tags.map((tag: pond.TagSettings) => (
<Grid2 key={"device-"+device.id()+"-tag-"+tag.key}>
<TagUI tag={Tag.any({ settings: tag })} />

View file

@ -1,4 +1,3 @@
import React from "react";
import PageContainer from "./PageContainer";
import FieldList from "field/FieldList";
import { Box } from "@mui/material";

View file

@ -206,7 +206,7 @@ export default function Firmware() {
// };
const load = useCallback(() => {
firmwareAPI.listFirmware(pageSize, pageSize * tablePage, "asc", "version", searchText)
firmwareAPI.listFirmware(pageSize, pageSize * tablePage, "desc", "version", searchText)
.then((response) => {
setTotal(response.data.total ? response.data.total : 0)
let rows: FirmwareModel[] = or(response.data.firmware, []).map(

View file

@ -339,6 +339,7 @@ export default function Gate(props: Props) {
}}
style={{ height: 150, width: "100%", margin: -15 }}>
<Grid
width="100%"
container
direction="row"
spacing={2}

View file

@ -34,7 +34,7 @@ export const HTTPContext = createContext<IHTTPContext>({} as IHTTPContext);
export default function HTTPProvider(props: Props) {
const { children, token } = props;
const { isAuthenticated, /*loginWithRedirect,*/ loginWithPopup } = useAuth0();
const { isAuthenticated, loginWithRedirect } = useAuth0();
const defaultOptions = (demo: boolean = false) => {
if (demo || !isAuthenticated || !token) {
@ -79,7 +79,7 @@ export default function HTTPProvider(props: Props) {
function get<T>(url: string, spreadOptions?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
if (isTokenExpired(token)) {
loginWithPopup()
loginWithRedirect()
}
return axios.get(url, {...defaultOptions(), ...spreadOptions});
}

View file

@ -1,6 +1,5 @@
import { useHTTP, usePermissionAPI } from "hooks";
// import { useWebsocket } from "websocket";
import { /*Component,*/ Component, deviceScope, Team, User } from "models";
import { Component, deviceScope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { createContext, PropsWithChildren, useContext } from "react";
import { pondURL } from "./pond";
@ -9,7 +8,6 @@ import { useGlobalState } from "providers";
import moment from "moment";
import { or } from "utils/types";
import { dateRange } from "providers/http";
// import { reject, result } from "lodash";
export interface IDeviceAPIContext {
add: (name: string, description: string, backpack: pond.BackpackSettings, otherTeam?: string) => Promise<AxiosResponse<pond.AddDeviceResponse>>;
@ -213,12 +211,27 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
types?: string[]
) => {
let url = pondURL("/deviceStatistics");
if (types && types.length > 0)
url = pondURL(
"/deviceStatistics/" +
(keys ? "?keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
if (types && types.length > 0) {
url = pondURL(
"/deviceStatistics/" +
(keys ? "?keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "")
)
if (!types.includes("team") && as.length > 0) {
url = pondURL(
"/deviceStatistics/" +
(keys ? "?keys=" + keys.toString() : "") +
(types ? "&types=" + types.toString() : "") +
("&as=" + as)
)
}
} else {
if (as.length > 0) {
url = pondURL(
"/deviceStatistics/?as=" + as
)
}
}
return new Promise<AxiosResponse<pond.GetDeviceStatisticsResponse>>((resolve, reject) => {
get<pond.GetDeviceStatisticsResponse>(url).then(resp => {
resp.data = pond.GetDeviceStatisticsResponse.fromObject(resp.data)

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,
@ -123,7 +123,7 @@ const ADAPTIVE_AGRICULTURE_WHITE_LABEL: WhiteLabel = {
// secondaryColour: "#FFFF00",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.REACT_APP_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID,
auth0ClientId: import.meta.env.VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://adaptiveagriculture.ca",
darkLogo: AdapativeAgLogo,
@ -163,7 +163,7 @@ const AEROGROW_WHITE_LABEL: WhiteLabel = {
secondaryColour: "cyan",
signatureColour: "#fff",
signatureAccentColour: "#000",
auth0ClientId: import.meta.env.REACT_APP_AUTH0_AEROGROW_CLIENT_ID,
auth0ClientId: import.meta.env.VITE_AUTH0_AEROGROW_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://www.aerogrowmanufacturing.com",
darkLogo: AeroGrowDarkLogo,
@ -189,7 +189,7 @@ const MIVENT_WHITE_LABEL: WhiteLabel = {
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.REACT_APP_AUTH0_MIVENT_CLIENT_ID,
auth0ClientId: import.meta.env.VITE_AUTH0_MIVENT_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://mivent.ca",
darkLogo: MiVentLightLogo,
@ -207,7 +207,7 @@ const ADAPTIVE_CONSTRUCTION_WHITE_LABEL: WhiteLabel = {
secondaryColour: "blue",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.REACT_APP_AUTH0_ADAPTIVE_CONSTRUCTION_CLIENT_ID,
auth0ClientId: import.meta.env.VITE_AUTH0_ADAPTIVE_CONSTRUCTION_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://adaptiveconstruction.ca",
darkLogo: AdConLogo,
@ -233,7 +233,7 @@ const OMNIAIR_WHITE_LABEL: WhiteLabel = {
secondaryColour: "yellow",
signatureColour: "#272727",
signatureAccentColour: "#fff",
auth0ClientId: import.meta.env.REACT_APP_AUTH0_OMNIAIR_CLIENT_ID,
auth0ClientId: import.meta.env.VITE_AUTH0_OMNIAIR_CLIENT_ID,
redirectOnLogout: true,
logoutRedirectTarget: "https://omniairsystems.com",
darkLogo: MiPCALogo,
@ -259,7 +259,7 @@ const whitelabels = new Map<string, WhiteLabel>([
["brandxducks", BXT_WHITE_LABEL],
["brandxtech", BXT_WHITE_LABEL],
["aerogrowmanufacturing", AEROGROW_WHITE_LABEL],
["localhost", STAGING_WHITELABEL],
["localhost", BXT_WHITE_LABEL],
["staging", STAGING_WHITELABEL],
["10.0", ADAPTIVE_CONSTRUCTION_WHITE_LABEL],
["mivent", MIVENT_WHITE_LABEL],
@ -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());

View file

@ -8,6 +8,7 @@ import { Task } from "models";
//import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import TasksIcon from "products/Construction/TasksIcon";
import moment from "moment";
import ButtonGroup from "common/ButtonGroup";
interface Props {
tasks: Task[];
@ -147,8 +148,21 @@ export default function TaskList(props: Props) {
return (
<Box padding="5px" textAlign="center">
<Typography variant="h5">{label ? label : ""}</Typography>
<Box textAlign="center">
<ToggleButtonGroup value={viewing} exclusive size="small" aria-label="Bin Mode">
<Box textAlign="center" display="flex" justifyContent="center">
<ButtonGroup
toggle
buttons={[
{
title: "Upcoming",
function: () => setViewing("upcoming")
},
{
title: "Complete",
function: () => setViewing("complete")
}
]}
/>
{/* <ToggleButtonGroup value={viewing} exclusive size="small" aria-label="Bin Mode">
<ToggleButton
value={"upcoming"}
aria-label="upcoming"
@ -161,7 +175,7 @@ export default function TaskList(props: Props) {
aria-label="complete">
Complete
</ToggleButton>
</ToggleButtonGroup>
</ToggleButtonGroup> */}
{location !== "/tasks" && (
<IconButton id="tasks" component={Link} to="/tasks">
<TasksIcon />

View file

@ -80,7 +80,7 @@ export default function TeamList() {
viewAs = undefined
}
teamAPI
.listTeams(limit, limit * page, undefined, "name", undefined, undefined, viewAs, search)
.listTeams(limit, limit * page, undefined, "name", undefined, undefined, " ", search)
.then(resp => {
setTotal(resp.data.total ? resp.data.total : 0);
let newTeams: Team[] = []

View file

@ -27,7 +27,7 @@ export default function TeamSearch(props: Props) {
if (!setSearch) return;
if (searchText === undefined) return;
setHandler(setTimeout(() => {
setSearchText(searchText);
setSearch(searchText);
}, 750)); // Delay of 750ms
return () => clearTimeout(handler); // Cleanup on change
@ -41,7 +41,7 @@ export default function TeamSearch(props: Props) {
let options: Option[] = [];
setLoading(true);
teamAPI
.listTeams(10, 0, "desc", undefined, undefined, undefined, undefined, search)
.listTeams(10, 0, "desc", undefined, undefined, undefined, " ", search)
.then(resp => {
if (loadUsers)
options.push({

View file

@ -29,7 +29,7 @@ export const AppThemeProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const savedTheme = localStorage.getItem("theme");
const isValidTheme = (theme: string | null): theme is ThemeMode =>
theme === "light" || theme === "dark" || theme === "system";
const [mode, setMode] = useState<ThemeMode>(isValidTheme(savedTheme) ? savedTheme : "system");
const [mode, setMode] = useState<ThemeMode>(isValidTheme(savedTheme) ? savedTheme : "dark");
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
const resolvedMode: PaletteMode = mode === "system" ? (prefersDark ? "dark" : "light") : mode;

View file

@ -17,6 +17,20 @@ const baseTheme: ThemeOptions = {
paper: '#f5f5f5',
},
},
typography: {
fontFamily: [
"Open Sans",
"-apple-system",
"BlinkMacSystemFont",
'"Segoe UI"',
'"Helvetica Neue"',
"Arial",
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"'
].join(",")
}
// Add other options if needed
};

View file

@ -73,14 +73,19 @@ export default function AccessObject(props: Props) {
const [teamsTotal, setTeamsTotal] = useState<number>(0);
const [devicesOffset, setDevicesOffset] = useState<number>(0);
const [devicesTotal, setDevicesTotal] = useState<number>(0);
const [prevAs, setPrevAs] = useState("")
const close = () => {
const close = (restoreAs?: boolean) => {
if(restoreAs){
dispatch({ key: "as", value: prevAs });
}
closeDialogCallback();
};
const getAccess = (key: number | string, name: string, url: string, useImitation = false) => {
user.settings.useTeam = false;
dispatch({ key: "as", value: "" });
//dispatch({ key: "as", value: "" });
userAPI.updateUser(user.id(), user.protobuf()).then(() => {
info("Will no longer view as team by default");
permissionAPI
@ -258,6 +263,12 @@ export default function AccessObject(props: Props) {
useEffect(() => {
if (isOpen && (prevSearchValue !== searchValue || prevKind !== kind || isOpen !== prevOpen)) {
//if the user is viewing as a team record who it was
if(as){
setPrevAs(as)
}
//change as so the user is viewing as themself
dispatch({ key: "as", value: "" });
load();
}
}, [
@ -350,12 +361,12 @@ export default function AccessObject(props: Props) {
return (
<ResponsiveDialog
open={isOpen}
onClose={close}
onClose={() => close(true)}
aria-labelledby="access-object-dialog"
fullScreen>
<AppBar position="sticky">
<Toolbar>
<IconButton edge="start" color="inherit" onClick={close} aria-label="close">
<IconButton edge="start" color="inherit" onClick={() => close(true)} aria-label="close">
<CloseIcon />
</IconButton>
<Typography variant="h6" noWrap>

View file

@ -22,8 +22,10 @@ export default defineConfig({
outDir: './build',
sourcemap: true,
minify: false,
target: 'esnext'
},
esbuild: {
keepNames: true, // Prevent function name mangling
}
},
})