diff --git a/.env b/.env index 90ef699..b1dbccb 100644 --- a/.env +++ b/.env @@ -7,6 +7,7 @@ VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=api.brandxtech.ca VITE_AUTH0_DEV_CLIENT_ID=dzJTpqIeMA4Rwk4xujtwPbAO3TY32bM1 VITE_AUTH0_STAGING_CLIENT_ID=3ib460VvLwdeyse5iUSQfxkVdQaUmphP +VITE_AUTH0_STREAMLINE_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_BXT_CLIENT_ID=sLnqOu40uWfQT1lYSDYj2wYmlLEHRB74 VITE_AUTH0_ADAPTIVE_AGRICULTURE_CLIENT_ID=5pUCkl2SfogkWmM244UDcLEUOp8EFdHd @@ -16,6 +17,9 @@ VITE_AUTH0_MIVENT_CLIENT_ID=VNALE7RW6l3dY5uYcxgwElZV0lcT25Fg VITE_AUTH0_OMNIAIR_CLIENT_ID=IblmarD8wFafiD6doxTmOHQ6Bx3L9wWl VITE_AUTH0_INTELLIFARMS_CLIENT_ID=RYtuAyOcB4DSaaqJMLDMf3pV8SFY9PdY +#Crisp +VITE_CRISP_WEBSITE_ID=80170383-b426-43c0-8f66-8e20a05bcdce + #Branding (Default theme) VITE_APP_WEBSITE_TITLE="Adaptive Dashboard" VITE_APP_PRIMARY_COLOUR=blue @@ -24,3 +28,6 @@ VITE_APP_SIGNATURE_COLOUR="#323232" # Live device/component WebSocket streams (must match string "true" in code) VITE_ENABLE_WEBSOCKETS=true + +# Default whitelabel; overwrite in .env.local +#VITE_WHITELABEL=adaptive-ag diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..0a84fdb --- /dev/null +++ b/deploy.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Load .env.local if it exists +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/.env.local" + +if [[ -f "$ENV_FILE" ]]; then + # Export only the vars we care about, ignoring comments and blank lines + set -o allexport + # shellcheck source=/dev/null + source <(grep -E '^(DEPLOY_USER|DEPLOY_HOST|DEPLOY_SSH_KEY)=' "$ENV_FILE") + set +o allexport +fi + +# Prompt for any missing values +if [[ -z "${DEPLOY_USER:-}" ]]; then + read -rp "SSH username: " DEPLOY_USER +fi + +if [[ -z "${DEPLOY_HOST:-}" ]]; then + read -rp "Server IP/hostname: " DEPLOY_HOST +fi + +if [[ -z "${DEPLOY_SSH_KEY:-}" ]]; then + read -rp "SSH key path [~/.ssh/id_ed25519]: " DEPLOY_SSH_KEY + DEPLOY_SSH_KEY="${DEPLOY_SSH_KEY:-~/.ssh/id_ed25519}" +fi + +SSH_OPTS="-i $DEPLOY_SSH_KEY" + +echo "→ Deploying to $DEPLOY_USER@$DEPLOY_HOST using key $DEPLOY_SSH_KEY" + +# 1. Build frontend for LAN / self-contained hosts (indexLocal.html via vite --mode localnet) +# Pass VITE_WHITELABEL if provided as an argument; otherwise Vite reads it from .env +( + cd "$SCRIPT_DIR" + if [[ -n "${1:-}" ]]; then + echo "→ Whitelabel override: $1" + VITE_WHITELABEL="$1" npm run build:local + else + npm run build:local + fi +) + +# Fail if the bundle still embeds Crisp (wrong vite mode or stale build/) +INDEX_HTML="$SCRIPT_DIR/build/index.html" +if [[ ! -f "$INDEX_HTML" ]]; then + echo "error: missing $INDEX_HTML after build:local" >&2 + exit 1 +fi + +# 2. Build container image +docker build -t webui:local -f Dockerfile . + +# 3. Transfer image +docker save webui:local | ssh $SSH_OPTS "$DEPLOY_USER@$DEPLOY_HOST" docker load + +# 4. SCP compose file +scp $SSH_OPTS docker-compose.local.yml "$DEPLOY_USER@$DEPLOY_HOST:~/" + +# 5. Deploy — then force service recreate. Swarm often keeps the old task when the tag stays +# webui:local after docker load, so without --force you still see the previous HTML/JS. +STACK_NAME="${LOCAL_DOCKER_STACK_NAME:-webui}" +SERVICE_NAME="${STACK_NAME}_webui" +ssh $SSH_OPTS "$DEPLOY_USER@$DEPLOY_HOST" \ + "docker stack deploy -c docker-compose.local.yml $STACK_NAME && docker service update --force $SERVICE_NAME" \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..b8722cb --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,18 @@ +version: "3.6" + +services: + webui: + image: webui:local + ports: + - "8080:80" + stop_signal: SIGQUIT + deploy: + replicas: 1 + update_config: + order: start-first + healthcheck: + test: ["CMD", "curl", "-f", "localhost:80/health"] + interval: 1m + timeout: 10s + retries: 3 + start_period: 30s \ No newline at end of file diff --git a/indexLocal.html b/indexLocal.html new file mode 100644 index 0000000..af3867c --- /dev/null +++ b/indexLocal.html @@ -0,0 +1,16 @@ + + + + + + + + Adaptive Dashboard + + + +
+ + + diff --git a/package-lock.json b/package-lock.json index a8d3068..58133fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "react-dom": "^18.3.1", "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", - "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", "react-infinite-scroller": "^1.2.6", "react-joyride": "^2.9.3", @@ -8617,12 +8616,6 @@ "node": ">=10" } }, - "node_modules/fscreen": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", - "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==", - "license": "MIT" - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -11516,21 +11509,6 @@ "is-lite": "^0.8.2" } }, - "node_modules/react-full-screen": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/react-full-screen/-/react-full-screen-1.1.1.tgz", - "integrity": "sha512-xoEgkoTiN0dw9cjYYGViiMCBYbkS97BYb4bHPhQVWXj1UnOs8PZ1rPzpX+2HMhuvQV1jA5AF9GaRbO3fA5aZtg==", - "license": "MIT", - "dependencies": { - "fscreen": "^1.0.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, "node_modules/react-horizontal-scrolling-menu": { "version": "7.1.8", "resolved": "https://registry.npmjs.org/react-horizontal-scrolling-menu/-/react-horizontal-scrolling-menu-7.1.8.tgz", diff --git a/package.json b/package.json index 7f5044c..50f1ab8 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "type": "module", "scripts": { "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-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 --mode localnet", "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-streamline": "VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.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", + "start-streamline": "VITE_WHITELABEL=streamline VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_APP_API_URL=https://streamline.api.adaptiveagriculture.ca/v1 VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca vite", + "start-staging": "VITE_WHITELABEL=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", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", @@ -16,6 +16,7 @@ "build:development": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} 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_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} 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:streamline": "VITE_CRISP_WEBSITE_ID={CRISP_WEBSITE_ID} VITE_AUTH0_CLIENT_ID=HwUV0hHNdVvU96zuMBTAU8i7nFdwwgIX VITE_AUTH0_CLIENT_DOMAIN=brandxtech.auth0.com VITE_AUTH0_AUDIENCE=streamline.api.adaptiveagriculture.ca VITE_APP_API_URL=https://streamline.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_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:local": "VITE_AUTH0_CLIENT_ID=local VITE_AUTH0_AUDIENCE=local VITE_AUTH0_CLIENT_DOMAIN=local VITE_APP_API_URL=http://172.16.1.20:50052/v1 VITE_APP_WS_URL=ws://172.16.1.20:50052/v1/live NODE_OPTIONS=--max_old_space_size=4096 vite build --mode localnet", "build:offline": "npx env-cmd offline,whitelabel npm run build", "test": "vitest" }, @@ -64,7 +65,6 @@ "react-dom": "^18.3.1", "react-emoji-render": "^2.0.1", "react-error-boundary": "^5.0.0", - "react-full-screen": "^1.1.1", "react-horizontal-scrolling-menu": "^7.1.1", "react-infinite-scroller": "^1.2.6", "react-joyride": "^2.9.3", diff --git a/src/app/App.tsx b/src/app/App.tsx index 2c6b94b..3616784 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,15 +1,20 @@ import { Auth0Provider } from '@auth0/auth0-react' import { or } from '../utils/types' +import { isAuth0Configured, isAuth0SpaOriginAllowed, shouldMountAuth0Provider } from '../utils/auth0Config' import AuthWrapper from '../providers/auth' import HTTPProvider from 'providers/http' import { useState } from 'react' import LoadingScreen from './LoadingScreen' +import LocalAuthPlaceholder from './LocalAuthPlaceholder' import UserWrapper from './UserWrapper' import { getWhitelabel } from 'services/whiteLabel' import { AppThemeProvider } from 'theme/AppThemeProvider' +import { LocalAuthProvider, Auth0AuthBridge } from '../providers/authContext' function App() { - const [token, setToken] = useState(undefined) + const [token, setToken] = useState(() => { + return localStorage.getItem('local_auth_token') || undefined + }) const whiteLabel = getWhitelabel() const manifestPath = "/" + whiteLabel.name.replace(/\s/g, "") + "/manifest.json" @@ -54,6 +59,29 @@ function App() { "/libracart" ] + if (!shouldMountAuth0Provider()) { + const placeholderReason = + isAuth0Configured() && !isAuth0SpaOriginAllowed() ? 'insecure_origin' : 'unconfigured' + + if (!token) { + return ( + + + + ) + } + + return ( + + + + + + + + ) + } + return ( {/* */} - { token ? - - - + { token ? + + + + + : void +} + +export default function LocalAuthPlaceholder(props: Props) { + const { reason = 'unconfigured', setToken } = props + const productName = getName() + + const [mode, setMode] = useState<'login' | 'signup'>('login') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [name, setName] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const apiUrl = import.meta.env.VITE_APP_API_URL + + const handleSignup = async () => { + setError('') + setLoading(true) + try { + const resp = await axios.post(`${apiUrl}/local-auth/signup`, { email, password, name }) + const token = resp.data.token + if (token && setToken) { + localStorage.setItem('local_auth_token', token) + setToken(token) + } + } catch (err: any) { + setError(err.response?.data?.error || 'Signup failed') + } finally { + setLoading(false) + } + } + + const handleLogin = async () => { + setError('') + setLoading(true) + try { + const resp = await axios.post(`${apiUrl}/local-auth/login`, { email, password }) + const token = resp.data.token + if (token && setToken) { + localStorage.setItem('local_auth_token', token) + setToken(token) + } + } catch (err: any) { + setError(err.response?.data?.error || 'Login failed') + } finally { + setLoading(false) + } + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (mode === 'signup') { + handleSignup() + } else { + handleLogin() + } + } + + const explanation = + reason === 'insecure_origin' + ? 'This URL is not a secure context for cloud sign-in. Use local account sign-in instead.' + : 'Local account sign-in for this deployment.' + + return ( + <> + + + +
+ + + {productName} + + + {explanation} + + {mode === 'signup' && ( + setName(e.target.value)} + fullWidth + /> + )} + setEmail(e.target.value)} + required + fullWidth + /> + setPassword(e.target.value)} + required + fullWidth + /> + {error && ( + + {error} + + )} + + + +
+
+
+ + ) +} diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index fac8487..5ac1f91 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import './App.css' import { useUserAPI } from '../providers/pond/userAPI' -import { useAuth0 } from '@auth0/auth0-react' import { or } from '../utils/types' import LoadingScreen from './LoadingScreen' import NavigationContainer from '../navigation/NavigationContainer' @@ -12,9 +11,8 @@ import { makeStyles } from '@mui/styles' import { CssBaseline, Theme } from '@mui/material' import { AppThemeProvider } from 'theme/AppThemeProvider' import HTTPProvider from 'providers/http' -import { Crisp } from "crisp-sdk-web"; import { useMobile, useSnackbar } from 'hooks' -import { initCrisp } from '../chat/CrispChat' +import { initCrisp, isCrispEnabled } from '../chat/CrispChat' // import FirmwareLoader from './FirmwareLoader' const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => { @@ -64,16 +62,19 @@ export default function UserWrapper(props: Props) { const [loading, setLoading] = useState(false) const userAPI = useUserAPI(); const classes = useStyles(); - const useAuth = useAuth0(); const hasFetched = useRef(false); const [global, setGlobal] = useState(undefined) const snackbar = useSnackbar() const isMobile = useMobile() - const user_id = or(useAuth.user?.sub, "") - - const crispInitialized = useRef(false); - Crisp.configure(import.meta.env.VITE_CRISP_WEBSITE_ID); + const user_id = (() => { + try { + const payload = JSON.parse(atob(token.split('.')[1])) + return or(payload.sub, '') + } catch { + return '' + } + })() const loadUser = useCallback(() => { if (!userAPI.getUserWithTeam) return; @@ -114,15 +115,14 @@ export default function UserWrapper(props: Props) { }, [setGlobal]) useEffect(() => { - if (global?.user) { - initCrisp({ - websiteId: import.meta.env.VITE_CRISP_WEBSITE_ID, - email: global.user.settings.email, - nickname: global.user.settings.name || global.user.settings.email, - phone: global.user.settings.phoneNumber, - tokenId: global.user.id(), - }); - } + if (!global?.user || !isCrispEnabled()) return; + initCrisp({ + websiteId: import.meta.env.VITE_CRISP_WEBSITE_ID, + email: global.user.settings.email, + nickname: global.user.settings.name || global.user.settings.email, + phone: global.user.settings.phoneNumber, + tokenId: global.user.id(), + }); }, [global]); // useEffect(() => { @@ -150,6 +150,7 @@ export default function UserWrapper(props: Props) { // }, [isMobile]); useEffect(() => { + if (!isCrispEnabled()) return; let style = document.getElementById("crisp-offset-override"); if (!style) { style = document.createElement("style"); @@ -194,4 +195,4 @@ export default function UserWrapper(props: Props) {
) -} \ No newline at end of file +} diff --git a/src/app/main.tsx b/src/app/main.tsx index 2d8615c..feae822 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -1,5 +1,6 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { StyledEngineProvider } from '@mui/material/styles' import App from './App' // I don't consider providing a disabled button to a Tooltip an error. @@ -20,6 +21,8 @@ if ('serviceWorker' in navigator) { createRoot(document.getElementById('root')!).render( // - + + + // , ) diff --git a/src/bin/AddBinFab.tsx b/src/bin/AddBinFab.tsx index 774785e..1eea8e3 100644 --- a/src/bin/AddBinFab.tsx +++ b/src/bin/AddBinFab.tsx @@ -28,6 +28,7 @@ const useStyles = makeStyles((theme: Theme) => { position: "fixed", bottom: theme.spacing(8), //for mobile navigator right: theme.spacing(2), + zIndex: theme.zIndex.speedDial, [theme.breakpoints.up("sm")]: { bottom: theme.spacing(1.75) } diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index cdfe14a..8eee39e 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -36,7 +36,6 @@ import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; import { useGlobalState, useSnackbar } from "providers"; import React, { useCallback, useEffect, useState } from "react"; -import { FullScreen, useFullScreenHandle } from "react-full-screen"; import moment, { Moment } from "moment"; import ResponsiveDialog from "common/ResponsiveDialog"; import { getThemeType } from "theme"; @@ -72,6 +71,7 @@ import ButtonGroup from "common/ButtonGroup"; import ModeChangeDialog from "./conditioning/modeChangeDialog"; import CustomGrainSelector from "grain/CustomGrainSelector"; import BinControllerDisplay from "./BinControllerDisplay"; +import { useFullScreen } from "hooks/FullScreenHandle"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -217,7 +217,7 @@ export default function BinVisualizer(props: Props) { const isMobile = useMobile(); const classes = useStyles(); const theme = useTheme(); - const fullScreenHandler = useFullScreenHandle(); + const {fullScreenHandler, FullScreenWrapper} = useFullScreen() const viewport = useViewport(); const { openSnack } = useSnackbar(); const [fillPercentage, setFillPercentage] = useState(0); @@ -1344,7 +1344,7 @@ export default function BinVisualizer(props: Props) { return ( - + - + ); }; diff --git a/src/chat/ChatDrawer.tsx b/src/chat/ChatDrawer.tsx index fa4ae4e..01eebcd 100644 --- a/src/chat/ChatDrawer.tsx +++ b/src/chat/ChatDrawer.tsx @@ -7,7 +7,7 @@ import Chat from "./Chat"; import { pond } from "protobuf-ts/pond"; import { useEffect, useState } from "react"; import { useTeamAPI } from "providers"; -import { closeCrispChat, openCrispChat } from './CrispChat'; +import { closeCrispChat, isCrispEnabled, openCrispChat } from './CrispChat'; import RobotIcon from "products/CommonIcons/robotIcon"; const useStyles = makeStyles((theme: Theme) => ({ @@ -68,7 +68,7 @@ export function ChatDrawer(props: Props) { } useEffect(() => { - if (open) { + if (open && isCrispEnabled()) { closeCrispChat() } }, [open]) @@ -108,13 +108,15 @@ export function ChatDrawer(props: Props) { - - - - - + {isCrispEnabled() && ( + + + + + + )} {teams.map((t, i)=> { if (t.settings?.key === team.key()) return null; diff --git a/src/chat/CrispChat.ts b/src/chat/CrispChat.ts index a0220fb..fe9fc16 100644 --- a/src/chat/CrispChat.ts +++ b/src/chat/CrispChat.ts @@ -5,6 +5,11 @@ const ANIMATION_DURATION_MS = 300; let initialized = false; +export function isCrispEnabled(): boolean { + const id = import.meta.env.VITE_CRISP_WEBSITE_ID; + return typeof id === "string" && id.trim() !== ""; +} + /** * Initialize Crisp and immediately hide the default chat button. * Call this once on app load (e.g., in UserWrapper after user data is ready). @@ -17,6 +22,7 @@ export function initCrisp(opts: { tokenId?: string; }) { if (initialized) return; + if (!opts.websiteId?.trim()) return; Crisp.configure(opts.websiteId); Crisp.session.reset(); @@ -71,6 +77,7 @@ function injectCrispStyles() { * Safe to call from any onClick handler. */ export function openCrispChat() { + if (!initialized) return; const chatbox = document.getElementById("crisp-chatbox"); if (chatbox) { chatbox.classList.add("crisp-visible"); @@ -86,6 +93,7 @@ export function openCrispChat() { * Close the chat window and hide the widget. */ export function closeCrispChat() { + if (!initialized) return; Crisp.chat.close(); hideCrispChatButton(); } diff --git a/src/component/ComponentCard.tsx b/src/component/ComponentCard.tsx index 6d430d5..8355abe 100644 --- a/src/component/ComponentCard.tsx +++ b/src/component/ComponentCard.tsx @@ -190,7 +190,18 @@ export default function ComponentCard(props: Props) { cableID = "Cable: " + (component.settings.addressType - 8); } return port + " " + cableID; - } else { + } else if (component.settings.addressType === quack.AddressType.ADDRESS_TYPE_I2C) { + let description = "" + let type = getFriendlyAddressTypeName(component.settings.addressType); + description = type + if(component.settings.expansionLine){ + description = description + ": Exp Line " + component.settings.expansionLine + } + if(component.settings.muxLine){ + description = description + " ,Mux Line " + component.settings.muxLine + } + return description + }else{ return getFriendlyAddressTypeName(component.settings.addressType); } }; diff --git a/src/device/DeviceTags.tsx b/src/device/DeviceTags.tsx index 7806a59..ab1d8a5 100644 --- a/src/device/DeviceTags.tsx +++ b/src/device/DeviceTags.tsx @@ -8,7 +8,6 @@ import { ListItem, ListItemIcon, ListItemText, - Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; @@ -19,10 +18,10 @@ import TagSettings from "common/TagSettings"; import { Device, Tag } from "models"; import { filterByTag } from "pbHelpers/Tag"; import { useDeviceAPI, useSnackbar, useTagAPI } from "providers"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { pond } from "protobuf-ts/pond"; -const useStyles = makeStyles((_theme: Theme) => { +const useStyles = makeStyles(() => { return ({ addIcon: { color: "var(--status-ok)" @@ -45,19 +44,19 @@ function AddDeviceTag(props: AddDeviceTagProps) { const [searchValue, setSearchValue] = useState(""); const [tags, setTags] = useState([]) - const loadTags = () => { + const loadTags = useCallback(() => { tagAPI.listTags().then(resp => { - let newTags: Tag[] = []; + const newTags: Tag[] = []; resp.data.tags.forEach((tag: pond.Tag) => { newTags.push(Tag.create(tag)) }) setTags(newTags) }) - } + }, [tagAPI]) useEffect(() => { loadTags() - }, []) + }, [loadTags]) const tagItems = tags .filter(tag => !deviceTags.some(tagSettings => tagSettings.key === tag.settings.key)) @@ -144,10 +143,10 @@ export default function DeviceTags(props: DeviceTagsProps) { const [deviceTags, setDeviceTags] = useState(device.status.tags); const previousDeviceRef = useRef(device); - const loadTags = () => { + const loadTags = useCallback(() => { // setLoading(true) tagAPI.listTags().then(resp => { - let newTags: Tag[] = []; + const newTags: Tag[] = []; resp.data.tags.forEach((tag: pond.Tag) => { newTags.push(Tag.create(tag)) }) @@ -155,7 +154,7 @@ export default function DeviceTags(props: DeviceTagsProps) { }).finally(() => { // setLoading(false) }) - } + }, [tagAPI]) useEffect(() => { if (previousDeviceRef.current !== device) { @@ -166,14 +165,16 @@ export default function DeviceTags(props: DeviceTagsProps) { useEffect(() => { loadTags() - }, []) + }, [loadTags]) const addTag = (tag: Tag) => { if (!deviceTags.some(dt => dt.key === tag.settings.key)) { deviceAPI .tag(device.id(), tag.settings.key) .then(() => { - setDeviceTags([...deviceTags, tag.settings]); + setDeviceTags(current => + current.some(dt => dt.key === tag.settings.key) ? current : [...current, tag.settings] + ); }) .catch(() => error("Failed to tag device as " + tag.name)); } @@ -184,7 +185,7 @@ export default function DeviceTags(props: DeviceTagsProps) { deviceAPI .untag(device.id(), tag.key()) .then(() => { - setDeviceTags(deviceTags.filter(t => tag.key() !== t.key)); + setDeviceTags(current => current.filter(t => tag.key() !== t.key)); }) .catch(() => error("Failed to remove tag " + tag.name + " from device")); } @@ -197,7 +198,7 @@ export default function DeviceTags(props: DeviceTagsProps) { return ( {deviceTags?.map(tagSettings => { - let pondTag = pond.Tag.create({ settings: tagSettings }) + const pondTag = pond.Tag.create({ settings: tagSettings }) return ( diff --git a/src/device/autoDetect/DeviceScannedComponents.tsx b/src/device/autoDetect/DeviceScannedComponents.tsx index dda2bf4..95c3759 100644 --- a/src/device/autoDetect/DeviceScannedComponents.tsx +++ b/src/device/autoDetect/DeviceScannedComponents.tsx @@ -12,6 +12,7 @@ import ResponsiveDialog from "common/ResponsiveDialog"; import { useComponentAPI, useDeviceAPI, useSnackbar } from "hooks"; import { ConfigurablePin } from "pbHelpers/AddressTypes"; import ScannedOneWirePort from "./OneWire/ScannedOneWirePort"; +import I2CExpander from "./I2CExpander"; interface Props { scannedComponents: pond.ComponentAddressMap @@ -27,6 +28,7 @@ interface CompStep { } let i2cBlacklistAddresses: number[] = [0x70] +let i2cExpanderAddresses: number[] = [0x71] export default function DeviceScannedComponents(props: Props){ const {scannedComponents, device, availablePositions, availableOffsets, refreshCallback} = props @@ -35,6 +37,7 @@ export default function DeviceScannedComponents(props: Props){ const [scannedI2C, setScannedI2C] = useState() //the unmodified scan of addresses const [scannedOneWire, setScannedOneWire] = useState([]) const [validI2CCompAddresses, setValidI2CComponentAddresses] = useState([]) //the filtered array of address data + const [expanderAddresses, setExpanderAddresses] = useState([]) const [components, setComponents] = useState([]) const [steps, setSteps] = useState([]); const { error, success } = useSnackbar(); @@ -69,18 +72,23 @@ export default function DeviceScannedComponents(props: Props){ useEffect(()=>{ let valid: quack.AddressData[] = [] + let expanders: quack.AddressData[] = [] //filter the address data let addr = scannedI2C?.settings?.foundAddresses console.log(addr) if(addr){ addr.forEach(addrData => { if(!i2cBlacklistAddresses.includes(addrData.address)){ - if(!(addrData.address === 0x71 && addrData.expansionLine === undefined)){ - valid.push(addrData) - } + if(i2cExpanderAddresses.includes(addrData.address) && addrData.expansionLine === 0){ + expanders.push(addrData) + }else{ + valid.push(addrData) + } } }) } + console.log(valid) + setExpanderAddresses(expanders) setValidI2CComponentAddresses(valid) },[scannedI2C]) @@ -257,33 +265,49 @@ export default function DeviceScannedComponents(props: Props){ {scannedI2C !== undefined && - - I2C Sensors + + I2C Scan - {validI2CCompAddresses.length > 0 ? validI2CCompAddresses.map((addr, index) => { - return ( - ()}/> - ) - }) : - - - No Sensors Found - + {expanderAddresses.length > 0 && + + Detected Expanders + {expanderAddresses.map((expAddr, index) => { + return ( + + ) + })} + + } + + {validI2CCompAddresses.length > 0 ? + + Detected Sensors + {validI2CCompAddresses.map((addr, index) => { + return ( + ()}/> + ) + })} + + : + + + No Sensors Found + + + } } - - } {scannedOneWire.length > 0 && - Pin Port Sensors + Pin Port Scan {scannedOneWire.map((scan,i) => { diff --git a/src/device/autoDetect/I2CExpander.tsx b/src/device/autoDetect/I2CExpander.tsx new file mode 100644 index 0000000..f2a8183 --- /dev/null +++ b/src/device/autoDetect/I2CExpander.tsx @@ -0,0 +1,27 @@ +import { Box, Typography } from "@mui/material" +import { quack } from "protobuf-ts/quack" + +interface Props { + address: quack.AddressData +} + + + +export default function I2CExpander(props: Props) { + const {address} = props + + const expanderType = () => { + switch(address.address){ + case 0x71: + return "Grain Cable Expander" + default: + return "Unknown Expander" + } + } + + return ( + + {expanderType()} {address.muxLine ? "Mux Line: " + address.muxLine : ""} + + ) +} \ No newline at end of file diff --git a/src/device/autoDetect/ScannedI2C.tsx b/src/device/autoDetect/ScannedI2C.tsx index 4719f66..3f1b461 100644 --- a/src/device/autoDetect/ScannedI2C.tsx +++ b/src/device/autoDetect/ScannedI2C.tsx @@ -108,6 +108,7 @@ export default function ScannedI2C(props: Props){ {sensorNum && Sensor {sensorNum}} {addressData.expansionLine !== 0 && "Expansion Line: " + addressData.expansionLine} + {/* may also want to specify the mux line, not sure if the sensors will have a mux line though */} {types.length > 0 ? diff --git a/src/hooks/FullScreenHandle.tsx b/src/hooks/FullScreenHandle.tsx new file mode 100644 index 0000000..9e4323b --- /dev/null +++ b/src/hooks/FullScreenHandle.tsx @@ -0,0 +1,121 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +/** + * Drop-in replacement for react-full-screen's useFullScreenHandle + FullScreen component. + * + * Motivation: react-full-screen 1.1.1 doesn't debounce fullscreenchange events, so on + * Windows systems with DPI scaling > 100%, the resize triggered during fullscreen entry + * fires a fullscreenchange event that the library misinterprets as an exit, immediately + * popping back out of fullscreen. + * + * This hook bypasses the library entirely and calls the native Fullscreen API directly, + * ignoring fullscreenchange events fired within DEBOUNCE_MS of entry to absorb the + * DPI-scaling resize blip. + * + * Usage — replace in BinVisualizerV2.tsx: + * + * // Remove: + * import { FullScreen, useFullScreenHandle } from "react-full-screen"; + * const fullScreenHandler = useFullScreenHandle(); + * ... + * + * // Add: + * import { useFullScreen } from "hooks/useFullScreen"; + * const { fullScreenHandler, FullScreenWrapper } = useFullScreen(); + * ... + * + * // Everything else (fullScreenHandler.active, .enter(), .exit()) stays the same. + */ + +const DEBOUNCE_MS = 500; + +export interface FullScreenHandle { + active: boolean; + enter: () => void; + exit: () => void; +} + +export function useFullScreen(): { + fullScreenHandler: FullScreenHandle; + FullScreenWrapper: React.FC<{ children: React.ReactNode }>; +} { + const [active, setActive] = useState(false); + const containerRef = useRef(null); + // Timestamp of the last enter() call — used to debounce spurious exit events + const enterTimeRef = useRef(0); + + const enter = useCallback(() => { + const el = containerRef.current; + if (!el) return; + enterTimeRef.current = Date.now(); + if (el.requestFullscreen) { + el.requestFullscreen().catch(() => { + // Some browsers (e.g. iOS Safari) reject the promise — silently ignore + }); + } else if ((el as any).webkitRequestFullscreen) { + (el as any).webkitRequestFullscreen(); + } else if ((el as any).mozRequestFullScreen) { + (el as any).mozRequestFullScreen(); + } else if ((el as any).msRequestFullscreen) { + (el as any).msRequestFullscreen(); + } + }, []); + + const exit = useCallback(() => { + if (document.exitFullscreen) { + document.exitFullscreen().catch(() => {}); + } else if ((document as any).webkitExitFullscreen) { + (document as any).webkitExitFullscreen(); + } else if ((document as any).mozCancelFullScreen) { + (document as any).mozCancelFullScreen(); + } else if ((document as any).msExitFullscreen) { + (document as any).msExitFullscreen(); + } + }, []); + + useEffect(() => { + const handleChange = () => { + const fullscreenEl = + document.fullscreenElement || + (document as any).webkitFullscreenElement || + (document as any).mozFullScreenElement || + (document as any).msFullscreenElement; + + const isNowFullscreen = fullscreenEl === containerRef.current; + + // If we just called enter() and this event fires within DEBOUNCE_MS, + // and it looks like an exit, ignore it — it's the DPI-scaling resize blip + if (!isNowFullscreen && Date.now() - enterTimeRef.current < DEBOUNCE_MS) { + return; + } + + setActive(isNowFullscreen); + }; + + document.addEventListener("fullscreenchange", handleChange); + document.addEventListener("webkitfullscreenchange", handleChange); + document.addEventListener("mozfullscreenchange", handleChange); + document.addEventListener("MSFullscreenChange", handleChange); + + return () => { + document.removeEventListener("fullscreenchange", handleChange); + document.removeEventListener("webkitfullscreenchange", handleChange); + document.removeEventListener("mozfullscreenchange", handleChange); + document.removeEventListener("MSFullscreenChange", handleChange); + }; + }, []); + + const FullScreenWrapper: React.FC<{ children: React.ReactNode }> = useCallback( + ({ children }) => ( +
+ {children} +
+ ), + [] + ); + + return { + fullScreenHandler: { active, enter, exit }, + FullScreenWrapper, + }; +} \ No newline at end of file diff --git a/src/navigation/BottomNavigator.tsx b/src/navigation/BottomNavigator.tsx index 6395bd4..60019a6 100644 --- a/src/navigation/BottomNavigator.tsx +++ b/src/navigation/BottomNavigator.tsx @@ -13,14 +13,14 @@ import BinsIcon from "products/Bindapt/BinsIcon"; import { useGlobalState } from "providers"; import { useCallback, useEffect, useState } from "react"; import { useNavigate, useLocation } from "react-router-dom"; -import { IsAdaptiveAgriculture, isBXT, IsMiVent, IsAdCon, IsOmniAir, IsMiPCA, IsIntellifarms } from "services/whiteLabel"; +import { getFeatures, getWhitelabel } from "services/whiteLabel"; import FieldsIcon from "products/AgIcons/FieldsIcon"; import NexusSTIcon from "products/Construction/NexusSTIcon"; import OmniAirDeviceIcon from "products/AviationIcons/OmniAirDeviceIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon"; import JobsiteIcon from "products/Construction/JobSiteIcon"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; interface Props { sideIsOpen: boolean; @@ -33,23 +33,19 @@ export default function BottomNavigator(props: Props) { const navigate = useNavigate(); const location = useLocation(); const prevLocation = usePrevious(location); - const { isAuthenticated } = useAuth0(); + const { isAuthenticated } = useAuthContext(); const [{ user }] = useGlobalState(); const [route, setRoute] = useState(sideIsOpen ? "side" : ""); - const isAg = IsAdaptiveAgriculture(); - const isIntel = IsIntellifarms(); - const isMiVent = IsMiVent(); - const isAdCon = IsAdCon(); - const isOmni = IsOmniAir(); - const isMiPCA = IsMiPCA(); + const wl = getWhitelabel(); + const f = getFeatures(); const reRoute = useCallback( (path: string) => { if (path === "") { - if (isAg || isIntel) { + if (f.bins) { return "bins"; } - if (isMiVent) { + if (f.ventilation) { return "ventilation"; } return "devices"; @@ -75,7 +71,7 @@ export default function BottomNavigator(props: Props) { } return path; }, - [isAg, isMiVent, isIntel] + [f] ); const autoDetectRoute = useCallback(() => { @@ -106,27 +102,27 @@ export default function BottomNavigator(props: Props) { const authenticatedNavigation = () => { return ( handleRouteChange(newValue)}> - {isAg || isIntel && ( + {f.visualFarm && ( } value="visualFarm" /> )} - {isAg || isIntel && ( + {f.bins && ( } value="bins" /> )} - {isAdCon && ( + {f.constructionMap && ( } value="constructionMap" /> )} - {(isOmni || isMiPCA) && ( + {f.aviationMap && ( } value="aviationMap" /> )} - {(isOmni || isMiPCA) && ( + {f.terminals && ( } @@ -137,11 +133,11 @@ export default function BottomNavigator(props: Props) { - ) : isAdCon ? ( + ) : f.constructionMap ? ( - ) : (isOmni || isMiPCA) ? ( + ) : f.aviationMap ? ( ) : ( @@ -149,22 +145,15 @@ export default function BottomNavigator(props: Props) { } value="devices" /> - {isAdCon && ( + {f.jobsites && ( } value="jobsites" /> )} - {/* {isMiVent && ( - } - value="ventilation" - /> - )} */} - {isBXT() && user.hasFeature("security") && ( + {f.security && user.hasFeature("security") && ( } value="security" /> )} } value="more" /> diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 94ce101..cde11f9 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -1,7 +1,7 @@ import { lazy, Suspense } from "react"; import LoadingScreen from "../app/LoadingScreen"; import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import Header from "app/Header"; import Logout from "pages/Logout"; import { ErrorBoundary } from "react-error-boundary"; @@ -57,7 +57,7 @@ export const appendToUrl = (appendage: number | string) => { export default function Router() { - const { /*isAuthenticated, loginWithRedirect,*/ isLoading } = useAuth0(); + const { isAuthenticated } = useAuthContext(); const whiteLabel = getWhitelabel(); const [{ user }] = useGlobalState(); @@ -306,13 +306,7 @@ export default function Router() { ); } - if (isLoading) return null; - // if (!isAuthenticated) { - // loginWithRedirect() - // return ( - // null - // ) - // } + if (!isAuthenticated) return null; function ErrorFallback({ error }: { error: Error }) { return
Something went wrong: {error.stack}
; diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 815f30b..47742dd 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -23,19 +23,9 @@ import BindaptIcon from "../products/Bindapt/BindaptIcon"; import { useLocation, useNavigate } from "react-router-dom"; import BinsIcon from "products/Bindapt/BinsIcon"; import { useGlobalState } from "providers"; -import { - IsAdaptiveAgriculture, - // hasTutorialPlaylist, - IsAdCon, - IsIntellifarms, - IsMiPCA, - // isBXT, - IsMiVent, - IsOmniAir, - IsStreamline, -} from "services/whiteLabel"; +import { getFeatures } from "services/whiteLabel"; import MiningIcon from "products/ventilation/MiningIcon"; -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import FieldsIcon from "products/AgIcons/FieldsIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; @@ -53,6 +43,7 @@ import CNHiIcon from "products/CommonIcons/cnhiIcon"; import LibraCartIcon from "products/CommonIcons/libracartIcon"; const drawerWidth = 230; +const closedDrawerWidth = 9.25; const useStyles = makeStyles((theme: Theme) => ({ sideMenu: { @@ -66,6 +57,8 @@ const useStyles = makeStyles((theme: Theme) => ({ sideMenuOpened: { zIndex: theme.zIndex.drawer + 2, width: drawerWidth, + minWidth: drawerWidth, + maxWidth: drawerWidth, transition: theme.transitions.create(["width"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen @@ -76,12 +69,16 @@ const useStyles = makeStyles((theme: Theme) => ({ easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen }), - // overflowX: "hidden", + overflowX: "hidden", width: theme.spacing(0), + minWidth: theme.spacing(0), + maxWidth: theme.spacing(0), // zIndex: theme.zIndex.drawer, // opacity: 0, [theme.breakpoints.up("md")]: { - width: theme.spacing(9.25), + width: theme.spacing(closedDrawerWidth), + minWidth: theme.spacing(closedDrawerWidth), + maxWidth: theme.spacing(closedDrawerWidth), opacity: 1 } }, @@ -130,7 +127,7 @@ interface Props { export default function SideNavigator(props: Props) { const { open, onOpen, onClose } = props; - const { isAuthenticated } = useAuth0() + const { isAuthenticated } = useAuthContext() const theme = useTheme(); const width = useWidth(); const classes = useStyles(); @@ -165,16 +162,10 @@ export default function SideNavigator(props: Props) { } const authenticatedSideMenu = () => { - const isMiVent = IsMiVent(); - const isAg = IsAdaptiveAgriculture() - const isIntel = IsIntellifarms() - const isStreamline = IsStreamline() - const isOmni = IsOmniAir() - const isMiPCA = IsMiPCA() - const isAdCon = IsAdCon() + const f = getFeatures(); return ( - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( + {(f.visualFarm || user.hasFeature("admin")) && ( )} - {((isOmni || isMiPCA) || user.hasFeature("admin")) && ( + {(f.aviationMap || user.hasFeature("admin")) && ( )} - {(isAdCon || user.hasFeature("admin")) && ( + {(f.constructionMap || user.hasFeature("admin")) && ( )} - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( + {(f.contracts || user.hasFeature("admin")) && ( )} - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( + {(f.bins || user.hasFeature("admin")) && ( )} - {((isOmni || isMiPCA) || user.hasFeature("admin")) && ( + {(f.terminals || user.hasFeature("admin")) && ( )} - {(user.hasFeature("installer") && isAg || user.hasFeature("admin")) && + {(f.cableEstimator && user.hasFeature("installer") || user.hasFeature("admin")) && } - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( + {(f.transactions || user.hasFeature("admin")) && ( )} - {(isMiVent || user.hasFeature("admin")) && ( + {(f.mines || user.hasFeature("admin")) && ( } - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( + {(f.fields || user.hasFeature("admin")) && ( )} - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( + {(f.grainTypes || user.hasFeature("admin")) && ( )} - {(isAdCon || user.hasFeature("admin")) && ( + {(f.jobsites || user.hasFeature("admin")) && ( )} - {(isAdCon || user.hasFeature("admin")) && ( + {(f.heaters || user.hasFeature("admin")) && ( } - {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && + {(f.marketplace || user.hasFeature("admin")) && - + - - {theme.direction === "rtl" ? : } + + {open + ? theme.direction === "rtl" ? : + : theme.direction === "rtl" ? : } @@ -552,4 +548,4 @@ export default function SideNavigator(props: Props) { {isAuthenticated && authenticatedSideMenu()} ); -} \ No newline at end of file +} diff --git a/src/pages/Bins.tsx b/src/pages/Bins.tsx index 7529f3f..7adde4e 100644 --- a/src/pages/Bins.tsx +++ b/src/pages/Bins.tsx @@ -58,9 +58,9 @@ import { // useBinYardAPI, useGlobalState, useInteractionsAPI, - useSnackbar, - useTeamAPI + useSnackbar } from "providers"; +import { useTeamAPI } from "providers/pond/teamAPI"; import React, { SetStateAction, useCallback, useEffect, useState } from "react"; // import { getThemeType } from "theme"; import { stringToMaterialColour } from "utils"; diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 55fd8b9..6c9535d 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -7,7 +7,8 @@ import { useDeviceStatusStreams } from "hooks/useDeviceStatusStreams"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useParams } from "react-router-dom"; import PageContainer from "./PageContainer"; -import { useHTTP, useMobile } from "hooks"; +import { useMobile } from "hooks"; +import { useHTTP } from "providers/http"; import LoadingScreen from "app/LoadingScreen"; import SmartBreadcrumb from "common/SmartBreadcrumb"; import DeviceActions from "device/DeviceActions"; @@ -623,4 +624,4 @@ export default function DevicePage() { {componentCards()} ); -} \ No newline at end of file +} diff --git a/src/pages/DeviceComponent.tsx b/src/pages/DeviceComponent.tsx index ad92c3a..a245e51 100644 --- a/src/pages/DeviceComponent.tsx +++ b/src/pages/DeviceComponent.tsx @@ -38,7 +38,8 @@ import { } from "pbHelpers/DeviceAvailability"; import { getDefaultInteraction } from "pbHelpers/Interaction"; import { pond } from "protobuf-ts/pond"; -import { useGlobalState, useTeamAPI } from "providers"; +import { useGlobalState } from "providers"; +import { useTeamAPI } from "providers/pond/teamAPI"; import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; // import { useRouteMatch } from "react-router"; diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index ea31e7f..54ff584 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -5,8 +5,9 @@ import { makeStyles } from "@mui/styles"; import ResponsiveTable, { Column } from "common/ResponsiveTable"; import ProvisionDevice from "device/ProvisionDevice"; import { pond } from "protobuf-ts/pond"; -import { useDeviceAPI, useGlobalState, useGroupAPI, useTeamAPI } from "providers"; -import { useHTTP } from "hooks"; +import { useDeviceAPI, useGlobalState, useGroupAPI } from "providers"; +import { useHTTP } from "providers/http"; +import { useTeamAPI } from "providers/pond/teamAPI"; import { useDeviceStatusStreams } from "hooks/useDeviceStatusStreams"; import { useCallback, useEffect, useMemo, useState } from "react"; import PageContainer from "./PageContainer"; @@ -1204,4 +1205,4 @@ export default function Devices() { /> ) -} \ No newline at end of file +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 3dc7d2f..ed56673 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,10 +1,11 @@ -import { RedirectLoginOptions, useAuth0 } from "@auth0/auth0-react"; +import { RedirectLoginOptions } from "@auth0/auth0-react"; // import { useAuth } from "hooks"; import queryString from "query-string"; import { useEffect } from "react"; import { useLocation } from "react-router"; // import Loading from "./Loading"; import LoadingScreen from "app/LoadingScreen"; +import { useAuthContext } from "providers/authContext"; // interface Props { // prevPath?: string; @@ -13,7 +14,7 @@ import LoadingScreen from "app/LoadingScreen"; export default function Login() { // const { prevPath } = props; const location = useLocation(); - const { loginWithRedirect } = useAuth0(); + const { loginWithRedirect } = useAuthContext(); // const setRouteBeforeLogin = useCallback((): Promise => { // return new Promise(function(resolve) { diff --git a/src/pages/Logout.tsx b/src/pages/Logout.tsx index 337c7b6..79e4538 100644 --- a/src/pages/Logout.tsx +++ b/src/pages/Logout.tsx @@ -1,9 +1,9 @@ -import { useAuth0 } from "@auth0/auth0-react"; +import { useAuthContext } from "providers/authContext"; import LoadingScreen from "app/LoadingScreen"; import { useEffect } from "react"; export default function Logout() { - const { logout } = useAuth0(); + const { logout } = useAuthContext(); useEffect(() => { logout(); diff --git a/src/pages/PageContainer.tsx b/src/pages/PageContainer.tsx index 3f0e605..5ff0a9d 100644 --- a/src/pages/PageContainer.tsx +++ b/src/pages/PageContainer.tsx @@ -1,4 +1,4 @@ -import { Container, SxProps, Theme } from "@mui/material"; +import { Container, SxProps, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import classNames from "classnames"; // import { useMobile } from "hooks"; @@ -47,6 +47,23 @@ const useStyles = makeStyles((theme: Theme) => ({ flexDirection: "column", justifyContent: "center", alignItems: "center" + }, + buildInfo: { + position: "fixed" as const, + bottom: 56, + left: 8, + fontSize: 10, + lineHeight: 1.3, + opacity: 0.35, + color: theme.palette.text.secondary, + pointerEvents: "none" as const, + zIndex: 1, + [theme.breakpoints.up("sm")]: { + bottom: 8, + }, + [theme.breakpoints.up("md")]: { + left: `calc(${theme.spacing(9)} + 8px)`, + } } })) @@ -78,8 +95,13 @@ export const PageContainer: React.FunctionComponent = (props: Props) => { }} disableGutters maxWidth={false} - children={<>{children}} - /> + > + {children} + + {new Date(__BUILD_DATE__).toLocaleDateString()}
+ {__GIT_HASH__} +
+ ); }; diff --git a/src/pages/SignupCallback.tsx b/src/pages/SignupCallback.tsx index e315eda..a8863f8 100644 --- a/src/pages/SignupCallback.tsx +++ b/src/pages/SignupCallback.tsx @@ -7,7 +7,7 @@ import { CheckCircleOutline } from "@mui/icons-material"; import { green } from "@mui/material/colors"; import Tour, { TourStep } from "common/Tour"; import Emoji from "react-emoji-render"; -import { getWhitelabel, IsAdaptiveAgriculture } from "services/whiteLabel"; +import { getWhitelabel, getFeatures } from "services/whiteLabel"; import { pond } from "protobuf-ts/pond"; import { User } from "models"; // import { color } from "framer-motion"; @@ -154,7 +154,7 @@ export default function SignupCallback () { Feet (ft)
- {IsAdaptiveAgriculture() && ( + {getFeatures().grainUnit && ( @@ -266,7 +266,7 @@ export default function TeamPage() { Groups - {((isAg || isStreamline) || user.hasFeature("admin")) && + {(f.bins || user.hasFeature("admin")) && <> diff --git a/src/providers/LoginButton.tsx b/src/providers/LoginButton.tsx index c8f9aac..e9e04a8 100644 --- a/src/providers/LoginButton.tsx +++ b/src/providers/LoginButton.tsx @@ -1,7 +1,7 @@ -import { useAuth0 } from '@auth0/auth0-react'; +import { useAuthContext } from './authContext'; const LoginButton = () => { - const { loginWithRedirect } = useAuth0(); + const { loginWithRedirect } = useAuthContext(); return (