diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b6339ad..f8bf5cc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -303,8 +303,8 @@ deploy:development: - "eval $(ssh-agent -s)" - ssh -V - cat /etc/os-release - - "echo DEPLOY_TARGET=${DEPLOY_TARGET}" - - "echo DOCKER_HOST=${DOCKER_HOST}" + # - "echo DEPLOY_TARGET=${DEPLOY_TARGET}" + # - "echo DOCKER_HOST=${DOCKER_HOST}" - "echo \"$DEVELOPMENT_SSH_PRIV_KEY\" | tr -d '\r' | ssh-add - " # - "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}" @@ -316,3 +316,98 @@ deploy:development: - docker stack deploy --with-registry-auth -c docker-compose.dev.yml webui when: manual allow_failure: false + +dependencies:streamline: + image: node:$NODE_VERSION + stage: dependencies + only: + refs: + - streamline + # before_script: + # - npm -g config set user root + script: + - npm ci + cache: + key: + files: + - package-lock.json + paths: + - node_modules + artifacts: + expire_in: 1 hour + 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 + +setup:streamline: + image: node:$NODE_VERSION + stage: setup + dependencies: + - dependencies:streamline + only: + - streamline + script: + - npm run build:streamline + artifacts: + expire_in: 1 hour + paths: + - build + +build:streamline: + image: docker:20.10.21 + stage: build + dependencies: + - setup:streamline + variables: + DOCKER_DRIVER: overlay2 + DOCKER_TLS_CERTDIR: "" + services: + - docker:20.10.21-dind + only: + - streamline + before_script: + - docker login -u ${CI_REGISTRY_USER} -p ${CI_JOB_TOKEN} ${CI_REGISTRY} + script: + - docker build -t ${CI_REGISTRY}/brandx/bxt-app/streamline:${CI_COMMIT_SHA} . + - docker tag ${CI_REGISTRY}/brandx/bxt-app/streamline:${CI_COMMIT_SHA} ${CI_REGISTRY}/brandx/bxt-app/streamline:latest + - docker push ${CI_REGISTRY}/brandx/bxt-app/streamline:${CI_COMMIT_SHA} + - docker push ${CI_REGISTRY}/brandx/bxt-app/streamline:latest + +deploy:streamline: + image: docker:20.10.21 + stage: deploy + variables: + DOCKER_DRIVER: overlay2 + DOCKER_HOST: ssh://deploy@streamline.adaptiveagriculture.ca + DEPLOY_IMAGE: ${CI_REGISTRY}/brandx/bxt-app/streamline:${CI_COMMIT_SHA} + DEPLOY_TARGET: deploy@streamline.adaptiveagriculture.ca + only: + - streamline + environment: + name: Streamline + url: https://streamline.adaptiveagriculture.ca + before_script: &deploy_tunnel + - "apk --update add openssh-client" + - "eval $(ssh-agent -s)" + - "echo \"$STREAMLINE_SSH_PRIV_KEY\" | tr -d '\r' | ssh-add - > /dev/null" + # - "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 + - chmod 700 ~/.ssh + - ssh -o StrictHostKeyChecking=no ${DEPLOY_TARGET} + script: + - docker login -u ${CI_REGISTRY_USER} -p ${CI_JOB_TOKEN} ${CI_REGISTRY} + - docker stack deploy --with-registry-auth -c docker-compose.streamline.yml webui + when: manual + allow_failure: false \ No newline at end of file diff --git a/docker-compose.streamline.yml b/docker-compose.streamline.yml new file mode 100644 index 0000000..218ab7d --- /dev/null +++ b/docker-compose.streamline.yml @@ -0,0 +1,33 @@ +version: "3.6" + +services: + webui: + image: ${DEPLOY_IMAGE} + networks: + - backend + stop_signal: SIGQUIT + deploy: + update_config: + order: start-first + healthcheck: + test: ["CMD", "curl", "localhost:8080/health"] + interval: 1m + timeout: 10s + retries: 3 + start_period: 30s + labels: + com.datadoghq.ad.logs: '[{"source": "nginx", "service": "webui"}]' + traefik.docker.network: "backend" + traefik.enable: "true" + traefik.http.routers.webui.rule: "Host(`streamline.adaptiveagriculture.ca`)" + traefik.frontend.redirect.regex: "^https?://streamline.adaptiveagriculture.ca/(.*)" + traefik.frontend.redirect.replacement: "https://streamline.adaptiveagriculture.ca/$${1}" + traefik.backend: "www" + traefik.http.services.webui.loadbalancer.server.port: "80" + traefik.protocol: "http" + dns: + - 69.27.101.6 + +networks: + backend: + external: true diff --git a/nginx.conf b/nginx.conf index a2c0d11..b390bff 100644 --- a/nginx.conf +++ b/nginx.conf @@ -63,6 +63,14 @@ http { expires $cache_expires; + location = /update { + return 302 /update.html; + } + + location = /update.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + # Redirect old /index.html requests to new file location = /index.html { return 301 /indexV2.html; @@ -79,7 +87,6 @@ http { } } - # Healthcheck server { listen 8080; diff --git a/package-lock.json b/package-lock.json index 407efa0..456c54d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6374,10 +6374,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6900,10 +6901,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7616,6 +7618,15 @@ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -8370,10 +8381,11 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -8402,6 +8414,18 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -10923,6 +10947,23 @@ "node": ">=6" } }, + "node_modules/query-string": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.2.1.tgz", + "integrity": "sha512-3jTGGLRzlhu/1ws2zlr4Q+GVMLCQTLFOj8CMX5x44cdZG9FQE07x2mQhaNxaKVPNmIDu0mvJ/cEwtY7Pim7hqA==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -12300,6 +12341,18 @@ "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", "integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==" }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", diff --git a/package.json b/package.json index 477f4c6..3287909 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "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-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", "build": "tsc -b && vite build", "lint": "eslint .", @@ -14,6 +15,7 @@ "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:streamline": "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:offline": "npx env-cmd offline,whitelabel npm run build", "test": "vitest" }, diff --git a/public/Streamline/favicon-16x16.png b/public/Streamline/favicon-16x16.png new file mode 100644 index 0000000..aa87654 Binary files /dev/null and b/public/Streamline/favicon-16x16.png differ diff --git a/public/Streamline/favicon-32x32.png b/public/Streamline/favicon-32x32.png new file mode 100644 index 0000000..a606ca0 Binary files /dev/null and b/public/Streamline/favicon-32x32.png differ diff --git a/public/Streamline/favicon.ico b/public/Streamline/favicon.ico new file mode 100644 index 0000000..54725be Binary files /dev/null and b/public/Streamline/favicon.ico differ diff --git a/public/Streamline/manifest.json b/public/Streamline/manifest.json new file mode 100644 index 0000000..7f8d80f --- /dev/null +++ b/public/Streamline/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "Streamline", + "short_name": "Streamline", + "icons": [ + { + "src": "favicon.ico", + "sizes": "48x48", + "type": "image/x-icon" + }, + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "maskable_icon.png", + "sizes": "196x196", + "type": "image/png", + "purpose": "any maskable" + } + ], + "start_url": "/", + "theme_color": "#272727", + "background_color": "#303030", + "display": "standalone", + "orientation": "any" +} diff --git a/public/Streamline/stream-logo.png b/public/Streamline/stream-logo.png new file mode 100644 index 0000000..c6d994b Binary files /dev/null and b/public/Streamline/stream-logo.png differ diff --git a/public/update.html b/public/update.html new file mode 100644 index 0000000..fcbeb4c --- /dev/null +++ b/public/update.html @@ -0,0 +1,44 @@ + + + + + Updating... + + + + +

Updating App…

+

Please wait while we refresh your version.

+ + + + diff --git a/src/assets/whitelabels/Streamline/stream-logo.png b/src/assets/whitelabels/Streamline/stream-logo.png new file mode 100644 index 0000000..c6d994b Binary files /dev/null and b/src/assets/whitelabels/Streamline/stream-logo.png differ diff --git a/src/bin/BinComponents.tsx b/src/bin/BinComponents.tsx index f99e349..7864c46 100644 --- a/src/bin/BinComponents.tsx +++ b/src/bin/BinComponents.tsx @@ -414,7 +414,7 @@ export default function BinComponents(props: Props) { )} - {comp.name() + " " + comp.addressDescription(devMap.get(device)?.settings.product)} + {comp.name() + " - " + comp.addressDescription(devMap.get(device)?.settings.product)} {components?.get(comp.key()) !== undefined ? ( diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index 1d722db..79238ea 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) => { }, stickyHeader: { position: "sticky", - backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)", + // backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)", top: 0, zIndex: 300 //giving a really high z-index to make sure nothing is in front of it } @@ -165,6 +165,7 @@ export default function ResponsiveTable(props: Props) { const saved = localStorage.getItem(rowKey); return saved ? JSON.parse(saved) : []; }) + useEffect(() => { localStorage.setItem(rowKey, JSON.stringify(rowWidths)); }, [rowWidths]) diff --git a/src/navigation/Router.tsx b/src/navigation/Router.tsx index 9ef6d2c..29362ad 100644 --- a/src/navigation/Router.tsx +++ b/src/navigation/Router.tsx @@ -53,7 +53,7 @@ export const appendToUrl = (appendage: number | string) => { export default function Router() { - const { isAuthenticated, loginWithRedirect, isLoading } = useAuth0(); + const { /*isAuthenticated, loginWithRedirect,*/ isLoading } = useAuth0(); const whiteLabel = getWhitelabel(); const [{ user }] = useGlobalState(); @@ -283,12 +283,12 @@ export default function Router() { }; if (isLoading) return null; - if (!isAuthenticated) { - loginWithRedirect() - return ( - null - ) - } + // if (!isAuthenticated) { + // loginWithRedirect() + // return ( + // null + // ) + // } function ErrorFallback({ error }: { error: Error }) { return
Something went wrong: {error.stack}
; @@ -302,6 +302,7 @@ export default function Router() { {/* Redirects */} } /> + {/* } /> */} {/* } /> */} {/* } /> */} } /> diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index fc902fb..38a10cb 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -30,6 +30,7 @@ import { // isBXT, IsMiVent, IsOmniAir, + IsStreamline, } from "services/whiteLabel"; import MiningIcon from "products/ventilation/MiningIcon"; import { useAuth0 } from "@auth0/auth0-react"; @@ -156,11 +157,12 @@ export default function SideNavigator(props: Props) { const authenticatedSideMenu = () => { const isMiVent = IsMiVent(); const isAg = IsAdaptiveAgriculture() + const isStreamline = IsStreamline() const isMiPCA = IsOmniAir() const isAdCon = IsAdCon() return ( - {(isAg || user.hasFeature("admin")) && ( + {(isAg || isStreamline || user.hasFeature("admin")) && ( )} - {(isAg || user.hasFeature("admin")) && ( + {(isAg || isStreamline || user.hasFeature("admin")) && ( )} - {(isAg || user.hasFeature("admin")) && ( + {(isAg || isStreamline || user.hasFeature("admin")) && ( } - {(isAg || user.hasFeature("admin")) && ( + {(isAg || isStreamline || user.hasFeature("admin")) && ( } - {(isAg || user.hasFeature("admin")) && ( + {(isAg || isStreamline || user.hasFeature("admin")) && ( } - {isAg && + {(isAg || isStreamline) && } - } + } ) } diff --git a/src/pages/Devices.tsx b/src/pages/Devices.tsx index db23b6c..fe47b3c 100644 --- a/src/pages/Devices.tsx +++ b/src/pages/Devices.tsx @@ -102,6 +102,8 @@ export default function Devices() { const [limit, setLimit] = useState(10); const [page, setPage] = useState(0); + const [{ user }] = useGlobalState() + const [order, setOrder] = useState<"asc" | "desc">(() => { // Load from sessionStorage on initial render const savedOrder = sessionStorage.getItem('order'); @@ -585,6 +587,7 @@ export default function Devices() { } const provisionButton= () => { + if (!user.allowedTo("provision")) return null return ( diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..3dc7d2f --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,53 @@ +import { RedirectLoginOptions, useAuth0 } 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"; + +// interface Props { +// prevPath?: string; +// } + +export default function Login() { + // const { prevPath } = props; + const location = useLocation(); + const { loginWithRedirect } = useAuth0(); + + // const setRouteBeforeLogin = useCallback((): Promise => { + // return new Promise(function(resolve) { + // localStorage.setItem("routeBeforeLogin", prevPath ? prevPath : "/"); + // return resolve("success"); + // }); + // }, [prevPath]); + + useEffect(() => { + const options: RedirectLoginOptions = { + authorizationParams: {} // Initialize here + }; + + if (location && location.search) { + let parsed = queryString.parse(location.search); + + if (parsed.signup === "true") { + // if (options.authorizationParams) options.authorizationParams.screen_hint = "signup"; // 'signup' tells Auth0 to show the signup tab + options.authorizationParams!.screen_hint = "signup"; // 'signup' tells Auth0 to show the signup tab + options.authorizationParams!.prompt = "login"; + } + + if (parsed.email && parsed.email !== "") { + // if (options.authorizationParams) options.authorizationParams.login_hint = parsed.email.toString(); // prefill email + options.authorizationParams!.login_hint = parsed.email.toString(); // prefill email + } + } + + // setRouteBeforeLogin().finally(() => { + console.log(options.authorizationParams) + loginWithRedirect(options); + // }); + }, [location, loginWithRedirect]); + + // return ; + return ; +} diff --git a/src/pages/PageContainer.tsx b/src/pages/PageContainer.tsx index 0c78ed6..524fbbf 100644 --- a/src/pages/PageContainer.tsx +++ b/src/pages/PageContainer.tsx @@ -1,7 +1,7 @@ import { Container, SxProps, Theme } from "@mui/material"; import { makeStyles } from "@mui/styles"; import classNames from "classnames"; -import { useMobile } from "hooks"; +// import { useMobile } from "hooks"; import { PropsWithChildren } from "react"; const useStyles = makeStyles((theme: Theme) => ({ @@ -44,11 +44,11 @@ interface Props extends PropsWithChildren{ export const PageContainer: React.FunctionComponent = (props: Props) => { const classes = useStyles(); - const isMobile = useMobile(); + // const isMobile = useMobile(); const { children, fullViewport, isCenterCenter, sx } = props; // let fullViewport = false // let isCenterCenter = false - const spacing = props.spacing; + const spacing = props.spacing ? props.spacing : 2; return ( { setToken: React.Dispatch> + prevPath?: string } export default function AuthWrapper(props: Props) { - const { children, setToken } = props; - const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently } = useAuth0(); - - useEffect(() => { - if (!isAuthenticated && !isLoading) { - loginWithRedirect(); - } - }, [isAuthenticated, loginWithRedirect, isLoading]); + const { children, setToken } = props; + const { isLoading, loginWithRedirect, isAuthenticated, getAccessTokenSilently } = useAuth0(); - useEffect(() => { - if (isAuthenticated) getAccessTokenSilently().then(t => { - if (t.length < 1) { - loginWithRedirect() - } - setToken(t) - }).catch(() => { - // No token, go back to login - loginWithRedirect() - }) - }, [setToken, isAuthenticated]) - return children; + useEffect(() => { + const url = new URL(window.location.href); + + const isManualLoginPage = url.pathname.includes("/login"); + + if (isManualLoginPage) { + const options: RedirectLoginOptions = { + authorizationParams: {} // Initialize here + }; + + let parsed = queryString.parse(url.search); + if (parsed.signup === "true") { + options.authorizationParams!.screen_hint = "signup"; + options.authorizationParams!.prompt = "login"; + } + + if (parsed.email && parsed.email !== "") { + options.authorizationParams!.login_hint = parsed.email.toString(); // prefill email + } + + loginWithRedirect(options) + + } else if (!isAuthenticated && !isLoading) { + loginWithRedirect(); + } + }, [isAuthenticated, loginWithRedirect, isLoading]); + + useEffect(() => { + if (isAuthenticated) getAccessTokenSilently().then(t => { + if (t.length < 1) { + loginWithRedirect() + } + setToken(t) + }).catch(() => { + // No token, go back to login + loginWithRedirect() + }) + }, [setToken, isAuthenticated]) + + return children; } \ No newline at end of file diff --git a/src/services/whiteLabel.ts b/src/services/whiteLabel.ts index d14ab51..0cd84cf 100644 --- a/src/services/whiteLabel.ts +++ b/src/services/whiteLabel.ts @@ -11,6 +11,7 @@ import AeroGrowLightLogo from "../assets/whitelabels/AeroGrow/lightLogo.png"; import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png"; // import OmniAirLogo from "../assets/whitelabels/OmniAir/OmniAirLogo.png"; import MiPCALogo from "../assets/whitelabels/OmniAir/MiPCALogo.png"; +import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png" // import { green, yellow } from "@mui/material/colors"; const protips: string[] = [ @@ -111,6 +112,28 @@ const BXT_WHITE_LABEL: WhiteLabel = { homePage: "/devices" }; +const STREAMLINE_WHITE_LABEL: WhiteLabel = { + name: "Streamline", + primaryColour: "#FFFF", + // primaryColour: "#0000FF", + secondaryColour: "yellow", + // secondaryColour: "#FFFF00", + // signatureColour: "#272727", + signatureColour: "#005bb0", + signatureAccentColour: "#fff", + auth0ClientId: import.meta.env.VITE_AUTH0_STREAMLINE_CLIENT_ID, + redirectOnLogout: false, + logoutRedirectTarget: "", + darkLogo: StreamlineLogo, + lightLogo: StreamlineLogo, + transparentLogoBG: false, + blacklist: [], + hotjarID: import.meta.env.REACT_APP_HOTJAR_ID_BXT, + docs: "Platform", + protips: protips, + homePage: "/devices" +}; + export function isBXT(): boolean { return getName() === "Brand X Technologies"; } @@ -157,6 +180,14 @@ export function IsAdaptiveAgriculture(): boolean { ); } +export function IsStreamline(): boolean { + return ( + getName() === "Streamline" + // window.location.origin.includes("staging") || + // window.location.origin.includes("localhost") + ); +} + const AEROGROW_WHITE_LABEL: WhiteLabel = { name: "AeroGrow", primaryColour: "green", @@ -254,6 +285,7 @@ export function IsOmniAir(): boolean { } const whitelabels = new Map([ + ["streamline", STREAMLINE_WHITE_LABEL], ["adaptiveag", ADAPTIVE_AGRICULTURE_WHITE_LABEL], ["adaptiveagriculture", ADAPTIVE_AGRICULTURE_WHITE_LABEL], ["brandxducks", BXT_WHITE_LABEL], @@ -276,6 +308,9 @@ export function getWhitelabel(): WhiteLabel { if (window.location.origin.includes("bxt-dev")) { return BXT_WHITE_LABEL; } + if (window.location.origin.includes("localhost")) { + return STREAMLINE_WHITE_LABEL; + } if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') { return STAGING_WHITELABEL; } diff --git a/src/teams/TeamActions.tsx b/src/teams/TeamActions.tsx index 63b335e..f12f9dc 100644 --- a/src/teams/TeamActions.tsx +++ b/src/teams/TeamActions.tsx @@ -146,6 +146,7 @@ export default function TeamActions(props: Props) { permissions={permissions} isDialogOpen={openState.share} closeDialogCallback={() => setOpenState({ ...openState, share: false })} + useImitation={false} /> ({ accessIcon: { @@ -76,7 +75,7 @@ const useStyles = makeStyles((theme: Theme) => ({ export default function UserMenu() { - const { toggleMode } = useThemeMode() + // const { toggleMode } = useThemeMode() const [{ user, team, as }, dispatch] = useGlobalState(); const { loginWithRedirect } = useAuth0(); const classes = useStyles(); @@ -163,10 +162,6 @@ export default function UserMenu() { )} - - {}} className={classes.rightIcon} aria-label="Toggle Theme"> - - ); }; diff --git a/src/user/UserSettings.tsx b/src/user/UserSettings.tsx index 32beec2..ec86924 100644 --- a/src/user/UserSettings.tsx +++ b/src/user/UserSettings.tsx @@ -10,10 +10,10 @@ import { Textsms as TextIcon, Contrast, } from "@mui/icons-material"; -import { AppBar, Box, Button, Checkbox, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Radio, RadioGroup, TextField, Theme, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, Typography } from "@mui/material"; +import { AppBar, Box, Button, Checkbox, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, TextField, Theme, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, Typography } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; import UserAvatar from "./UserAvatar"; -import { useGlobalState, useUserAPI } from "providers"; +import { useGlobalState, useSnackbar, useUserAPI } from "providers"; import { useEffect, useState } from "react"; import { makeStyles } from "@mui/styles"; import { blue } from "@mui/material/colors"; @@ -26,8 +26,11 @@ import moment from "moment"; import { IconPicker } from "common/IconPicker"; import { MuiTelInput } from "mui-tel-input"; import { useThemeMode } from "theme/AppThemeProvider"; -import ContractsIcon from "products/CommonIcons/contractIcon"; +// import ContractsIcon from "products/CommonIcons/contractIcon"; +import UnitsIcon from "@mui/icons-material/Category"; import { setThemeType } from "theme"; +import { IsAdaptiveAgriculture } from "services/whiteLabel"; +import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils"; const useStyles = makeStyles((theme: Theme) => { return ({ @@ -71,14 +74,16 @@ export default function UserSettings(props: Props) { const [sharing, setSharing] = useState(false) const userAPI = useUserAPI(); const classes = useStyles() + const { success } = useSnackbar(); const [isLoading, setIsLoading] = useState(false); const [user, setUser] = useState(globalState.user); const [profileExpanded, setProfileExpanded] = useState(false); const [avatarExpanded, setAvatarExpanded] = useState(false); const [notificationsExpanded, setNotificationsExpanded] = useState(false); + const [unitsExpanded, setUnitsExpanded] = useState(false); const [avatarUrl, setAvatarUrl] = useState(""); - const [themeValue, setThemeValue] = useState<"light" | "dark" | "system">(themeMode.mode) + // const [themeValue, setThemeValue] = useState<"light" | "dark" | "system">(themeMode.mode) useEffect(() => { function changeIcon(icon: string | undefined) { @@ -99,11 +104,11 @@ export default function UserSettings(props: Props) { .updateUser(user.id(), user.protobuf()) .then(() => { dispatch({ key: "user", value: user }); - // success("User settings successfully updated!"); - // setTemperatureUnit(user.settings.temperatureUnit); - // setPressureUnit(user.settings.pressureUnit); - // setDistanceUnit(user.settings.distanceUnit); - // setGrainUnit(user.settings.grainUnit); + success("User settings successfully updated!"); + setTemperatureUnit(user.settings.temperatureUnit); + setPressureUnit(user.settings.pressureUnit); + setDistanceUnit(user.settings.distanceUnit); + setGrainUnit(user.settings.grainUnit); }) .catch((error: any) => { error("Error occurred while updating your profile"); @@ -362,6 +367,160 @@ export default function UserSettings(props: Props) { setThemeType(theme) }; + const changePressureUnit = (value: any) => { + let updatedUser = User.clone(user); + let pressureUnit: pond.PressureUnit; + switch (value) { + case 1: + pressureUnit = pond.PressureUnit.PRESSURE_UNIT_KILOPASCALS; + break; + case 2: + pressureUnit = pond.PressureUnit.PRESSURE_UNIT_INCHES_OF_WATER; + break; + default: + pressureUnit = pond.PressureUnit.PRESSURE_UNIT_UNKNOWN; + break; + } + updatedUser.settings.pressureUnit = pressureUnit; + setUser(updatedUser); + }; + + const changeTemperatureUnit = (value: any) => { + let updatedUser = User.clone(user); + let temperatureUnit: pond.TemperatureUnit; + switch (value) { + case 1: + temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_CELSIUS; + break; + case 2: + temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT; + break; + default: + temperatureUnit = pond.TemperatureUnit.TEMPERATURE_UNIT_UNKNOWN; + break; + } + updatedUser.settings.temperatureUnit = temperatureUnit; + setUser(updatedUser); + }; + + const changeDistanceUnit = (value: any) => { + let updatedUser = User.clone(user); + let distanceUnit: pond.DistanceUnit; + switch (value) { + case 1: + distanceUnit = pond.DistanceUnit.DISTANCE_UNIT_FEET; + break; + case 2: + distanceUnit = pond.DistanceUnit.DISTANCE_UNIT_METERS; + break; + default: + distanceUnit = pond.DistanceUnit.DISTANCE_UNIT_UNKNOWN; + break; + } + updatedUser.settings.distanceUnit = distanceUnit; + setUser(updatedUser); + }; + + const changeGrainUnit = (value: any) => { + let updatedUser = User.clone(user); + let grainUnit: pond.GrainUnit; + switch (value) { + case 1: + grainUnit = pond.GrainUnit.GRAIN_UNIT_BUSHELS; + break; + case 2: + grainUnit = pond.GrainUnit.GRAIN_UNIT_WEIGHT; + break; + default: + grainUnit = pond.GrainUnit.GRAIN_UNIT_UNKNOWN; + break; + } + updatedUser.settings.grainUnit = grainUnit; + setUser(updatedUser); + }; + + const unitPreferences = () => { + const { pressureUnit, temperatureUnit, distanceUnit, grainUnit } = user.settings; + return ( + + + changePressureUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + + Kilopascals (kPa) + + + Inches of Water (iwg) + + + + + changeTemperatureUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + Celsius (°C) + + Fahrenheit (°F) + + + + + changeDistanceUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + Meters (m) + Feet (ft) + + + {IsAdaptiveAgriculture() && ( + + changeGrainUnit(event.target.value)} + margin="normal" + variant="outlined" + InputLabelProps={{ shrink: true }}> + Bushels (bu) + Tonnes (mT) + + + )} + + ); + }; + const themeToggle = () => { return ( - {/* + - setUnitsExpanded(!unitsExpanded)}> + setUnitsExpanded(!unitsExpanded)}> {unitsExpanded ? : } - + @@ -472,7 +631,7 @@ export default function UserSettings(props: Props) { - + {/* setMapsExpanded(!mapsExpanded)}>