From e95b654d7f579d867a1d5ba4c284ca2c6fd53b81 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 18 Nov 2024 10:10:29 -0600 Subject: [PATCH] teams list now renders --- package-lock.json | 271 +++++++ package.json | 4 + src/common/DeleteButton.tsx | 35 + src/common/IconPicker.tsx | 230 ++++++ src/common/NotificationButton.tsx | 41 + src/common/ResponsiveDialog.tsx | 38 + src/hooks/index.ts | 8 +- src/hooks/usePrevious.ts | 9 + src/models/ShareableLink.ts | 32 + src/models/index.ts | 2 +- src/pages/Teams.tsx | 6 +- src/pbHelpers/Context.ts | 27 + src/pbHelpers/Permission.ts | 21 + src/pbHelpers/User.ts | 41 + src/products/AgIcons/AddField.tsx | 24 + src/products/AgIcons/AddMarker.tsx | 24 + src/products/AgIcons/AerationFanIcon.tsx | 29 + src/products/AgIcons/Delete.tsx | 24 + src/products/AgIcons/DiseaseIcon.tsx | 25 + src/products/AgIcons/Edit.tsx | 24 + src/products/AgIcons/FieldList.tsx | 24 + src/products/AgIcons/FieldMap.tsx | 24 + src/products/AgIcons/FieldNames.tsx | 24 + src/products/AgIcons/FieldsIcon.tsx | 29 + src/products/AgIcons/GearMarker.tsx | 24 + src/products/AgIcons/HomeIcon.tsx | 24 + src/products/AgIcons/MarkerMove.tsx | 24 + src/products/AgIcons/PestsIcon.tsx | 25 + src/products/AgIcons/PressureIcon.tsx | 37 + src/products/AgIcons/RocksIcon.tsx | 25 + src/products/AgIcons/ScoutIcon.tsx | 24 + src/products/AgIcons/WeedsIcon.tsx | 25 + src/products/AviationIcons/AddPlaneIcon.tsx | 25 + src/products/AviationIcons/AirportMapIcon.tsx | 25 + .../AviationIcons/OmniAirDeviceIcon.tsx | 24 + src/products/AviationIcons/PlaneIcon.tsx | 25 + src/products/Bindapt/AddBinIcon.tsx | 24 + src/products/Bindapt/BindaptAvailability.ts | 292 +++++++ src/products/Bindapt/BindaptDescriber.ts | 210 +++++ src/products/Bindapt/BinsIcon.tsx | 39 + src/products/Bindapt/CableIcon.tsx | 24 + src/products/Bindapt/DataDuckIcon.tsx | 24 + src/products/Bindapt/constructionIcon.tsx | 11 + src/products/Bindapt/index.ts | 4 + src/products/CommonIcons/cnhiIcon.tsx | 24 + src/products/CommonIcons/contractIcon.tsx | 29 + src/products/CommonIcons/fuelIcon.tsx | 24 + src/products/CommonIcons/graphIcon.tsx | 24 + src/products/CommonIcons/johnDeereIcon.tsx | 24 + src/products/CommonIcons/marketplaceIcon.tsx | 24 + src/products/Construction/ACHomeIcon.tsx | 24 + src/products/Construction/DeviceGroupIcon.tsx | 29 + src/products/Construction/JobSiteIcon.tsx | 28 + src/products/Construction/NexusSTIcon.tsx | 29 + .../Construction/ObjectHeaterIcon.tsx | 24 + src/products/Construction/SitesIcon.tsx | 29 + src/products/Construction/TasksIcon.tsx | 29 + src/products/DeviceProduct.ts | 140 ++++ src/products/Nexus/NexusAvailability.ts | 31 + src/products/Nexus/NexusDescriber.ts | 17 + src/products/OmniAir/OmniAirAvailability.ts | 59 ++ src/products/OmniAir/OmniAirDescriber.ts | 31 + src/products/ProductTabChip.tsx | 86 +++ src/products/ventilation/MiningIcon.tsx | 24 + src/products/ventilation/VentilationIcon.tsx | 24 + src/providers/Snackbar.tsx | 141 ++++ src/providers/index.ts | 13 +- src/providers/pond/binAPI.tsx | 499 ++++++++++++ src/providers/pond/gateAPI.tsx | 267 +++++++ src/providers/pond/imagekitAPI.tsx | 45 ++ src/providers/pond/permissionAPI.tsx | 153 ++++ src/providers/pond/pond.tsx | 21 +- src/providers/pond/teamAPI.tsx | 4 +- src/providers/pond/userAPI.tsx | 40 +- src/teams/TeamActions.tsx | 197 +++++ src/teams/TeamList.tsx | 360 +++++++++ src/teams/TeamSettings.tsx | 265 +++++++ src/user/ObjectUsers.tsx | 719 ++++++++++++++++++ src/user/RemoveSelfFromObject.tsx | 111 +++ src/user/ShareObject.tsx | 601 +++++++++++++++ src/utils/environment.ts | 32 + 81 files changed, 6132 insertions(+), 40 deletions(-) create mode 100644 src/common/DeleteButton.tsx create mode 100644 src/common/IconPicker.tsx create mode 100644 src/common/NotificationButton.tsx create mode 100644 src/common/ResponsiveDialog.tsx create mode 100644 src/hooks/usePrevious.ts create mode 100644 src/models/ShareableLink.ts create mode 100644 src/pbHelpers/Context.ts create mode 100644 src/pbHelpers/Permission.ts create mode 100644 src/pbHelpers/User.ts create mode 100644 src/products/AgIcons/AddField.tsx create mode 100644 src/products/AgIcons/AddMarker.tsx create mode 100644 src/products/AgIcons/AerationFanIcon.tsx create mode 100644 src/products/AgIcons/Delete.tsx create mode 100644 src/products/AgIcons/DiseaseIcon.tsx create mode 100644 src/products/AgIcons/Edit.tsx create mode 100644 src/products/AgIcons/FieldList.tsx create mode 100644 src/products/AgIcons/FieldMap.tsx create mode 100644 src/products/AgIcons/FieldNames.tsx create mode 100644 src/products/AgIcons/FieldsIcon.tsx create mode 100644 src/products/AgIcons/GearMarker.tsx create mode 100644 src/products/AgIcons/HomeIcon.tsx create mode 100644 src/products/AgIcons/MarkerMove.tsx create mode 100644 src/products/AgIcons/PestsIcon.tsx create mode 100644 src/products/AgIcons/PressureIcon.tsx create mode 100644 src/products/AgIcons/RocksIcon.tsx create mode 100644 src/products/AgIcons/ScoutIcon.tsx create mode 100644 src/products/AgIcons/WeedsIcon.tsx create mode 100644 src/products/AviationIcons/AddPlaneIcon.tsx create mode 100644 src/products/AviationIcons/AirportMapIcon.tsx create mode 100644 src/products/AviationIcons/OmniAirDeviceIcon.tsx create mode 100644 src/products/AviationIcons/PlaneIcon.tsx create mode 100644 src/products/Bindapt/AddBinIcon.tsx create mode 100644 src/products/Bindapt/BindaptAvailability.ts create mode 100644 src/products/Bindapt/BindaptDescriber.ts create mode 100644 src/products/Bindapt/BinsIcon.tsx create mode 100644 src/products/Bindapt/CableIcon.tsx create mode 100644 src/products/Bindapt/DataDuckIcon.tsx create mode 100644 src/products/Bindapt/constructionIcon.tsx create mode 100644 src/products/Bindapt/index.ts create mode 100644 src/products/CommonIcons/cnhiIcon.tsx create mode 100644 src/products/CommonIcons/contractIcon.tsx create mode 100644 src/products/CommonIcons/fuelIcon.tsx create mode 100644 src/products/CommonIcons/graphIcon.tsx create mode 100644 src/products/CommonIcons/johnDeereIcon.tsx create mode 100644 src/products/CommonIcons/marketplaceIcon.tsx create mode 100644 src/products/Construction/ACHomeIcon.tsx create mode 100644 src/products/Construction/DeviceGroupIcon.tsx create mode 100644 src/products/Construction/JobSiteIcon.tsx create mode 100644 src/products/Construction/NexusSTIcon.tsx create mode 100644 src/products/Construction/ObjectHeaterIcon.tsx create mode 100644 src/products/Construction/SitesIcon.tsx create mode 100644 src/products/Construction/TasksIcon.tsx create mode 100644 src/products/DeviceProduct.ts create mode 100644 src/products/Nexus/NexusAvailability.ts create mode 100644 src/products/Nexus/NexusDescriber.ts create mode 100644 src/products/OmniAir/OmniAirAvailability.ts create mode 100644 src/products/OmniAir/OmniAirDescriber.ts create mode 100644 src/products/ProductTabChip.tsx create mode 100644 src/products/ventilation/MiningIcon.tsx create mode 100644 src/products/ventilation/VentilationIcon.tsx create mode 100644 src/providers/Snackbar.tsx create mode 100644 src/providers/pond/binAPI.tsx create mode 100644 src/providers/pond/gateAPI.tsx create mode 100644 src/providers/pond/imagekitAPI.tsx create mode 100644 src/providers/pond/permissionAPI.tsx create mode 100644 src/teams/TeamActions.tsx create mode 100644 src/teams/TeamList.tsx create mode 100644 src/teams/TeamSettings.tsx create mode 100644 src/user/ObjectUsers.tsx create mode 100644 src/user/RemoveSelfFromObject.tsx create mode 100644 src/user/ShareObject.tsx create mode 100644 src/utils/environment.ts diff --git a/package-lock.json b/package-lock.json index 991e368..090508f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,14 @@ "@mui/icons-material": "^6.1.6", "@mui/material": "^6.1.6", "@mui/styles": "^6.1.6", + "@mui/x-date-pickers": "^7.22.2", + "@sentry/react": "^8.38.0", "@types/classnames": "^2.3.0", "axios": "^1.7.7", "lodash": "^4.17.21", "moment": "^2.30.1", + "moment-timezone": "^0.5.46", + "notistack": "^3.0.1", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -1448,6 +1452,92 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "7.22.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz", + "integrity": "sha512-1KHSlIlnSoY3oHm820By8X344pIdGYqPvCCvfVHrEeeIQ/pHdxDD8tjZFWkFl4Jgm9oVFK90fMcqNZAzc+WaCw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.21.0", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.21.0.tgz", + "integrity": "sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1793,6 +1883,135 @@ "win32" ] }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.38.0.tgz", + "integrity": "sha512-5QMVcssrAcmjKT0NdFYcX0b0wwZovGAZ9L2GajErXtHkBenjI2sgR2+5J7n+QZGuk2SC1qhGmT1O9i3p3UEwew==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.38.0.tgz", + "integrity": "sha512-AW5HCCAlc3T1jcSuNhbFVNO1CHyJ5g5tsGKEP4VKgu+D1Gg2kZ5S2eFatLBUP/BD5JYb1A7p6XPuzYp1XfMq0A==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.38.0.tgz", + "integrity": "sha512-mQPShKnIab7oKwkwrRxP/D8fZYHSkDY+cvqORzgi+wAwgnunytJQjz9g6Ww2lJu98rHEkr5SH4V4rs6PZYZmnQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.38.0.tgz", + "integrity": "sha512-OxmlWzK9J8mRM+KxdSnQ5xuxq+p7TiBzTz70FT3HltxmeugvDkyp6803UcFqHOPHR35OYeVLOalym+FmvNn9kw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.38.0.tgz", + "integrity": "sha512-AZR+b0EteNZEGv6JSdBD22S9VhQ7nrljKsSnzxobBULf3BpwmhmCzTbDrqWszKDAIDYmL+yQJIR2glxbknneWQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.38.0", + "@sentry-internal/feedback": "8.38.0", + "@sentry-internal/replay": "8.38.0", + "@sentry-internal/replay-canvas": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.38.0.tgz", + "integrity": "sha512-sGD+5TEHU9G7X7zpyaoJxpOtwjTjvOd1f/MKBrWW2vf9UbYK+GUJrOzLhMoSWp/pHSYgvObkJkDb/HwieQjvhQ==", + "license": "MIT", + "dependencies": { + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.38.0.tgz", + "integrity": "sha512-5396tewO00wbJFHUkmU+ikmp4A+wuBpStNc7UDyAm642jfbPajj51+GWld/ZYNFiQaZ/8I9tvvpHqVLnUh21gg==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "8.38.0", + "@sentry/core": "8.38.0", + "@sentry/types": "8.38.0", + "@sentry/utils": "8.38.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.38.0.tgz", + "integrity": "sha512-fP5H9ZX01W4Z/EYctk3mkSHi7d06cLcX2/UWqwdWbyPWI+pL2QpUPICeO/C+8SnmYx//wFj3qWDhyPCh1PdFAA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-3X7MgIKIx+2q5Al7QkhaRB4wV6DvzYsaeIwdqKUzGLuRjXmNgJrLoU87TAwQRmZ6Wr3IoEpThZZMNrzYPXxArw==", + "license": "MIT", + "dependencies": { + "@sentry/types": "8.38.0" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3285,6 +3504,15 @@ "dev": true, "license": "MIT" }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3769,6 +3997,18 @@ "node": "*" } }, + "node_modules/moment-timezone": { + "version": "0.5.46", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", + "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3808,6 +4048,37 @@ "dev": true, "license": "MIT" }, + "node_modules/notistack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/notistack/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index 289269d..fc44ecc 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,14 @@ "@mui/icons-material": "^6.1.6", "@mui/material": "^6.1.6", "@mui/styles": "^6.1.6", + "@mui/x-date-pickers": "^7.22.2", + "@sentry/react": "^8.38.0", "@types/classnames": "^2.3.0", "axios": "^1.7.7", "lodash": "^4.17.21", "moment": "^2.30.1", + "moment-timezone": "^0.5.46", + "notistack": "^3.0.1", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/common/DeleteButton.tsx b/src/common/DeleteButton.tsx new file mode 100644 index 0000000..00112c3 --- /dev/null +++ b/src/common/DeleteButton.tsx @@ -0,0 +1,35 @@ +import { Delete } from "@mui/icons-material"; +import { Theme, Button, ButtonProps } from "@mui/material"; +import { red } from "@mui/material/colors"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + deleteButton: { + color: "#fff", + background: red["500"], + "&:hover": { + background: red["600"] + } + }, + rightIcon: { + marginLeft: theme.spacing(1) + } +})); + +const DeleteButton = (props: ButtonProps) => { + const classes = useStyles(); + + return ( + + ); +}; + +export default DeleteButton; diff --git a/src/common/IconPicker.tsx b/src/common/IconPicker.tsx new file mode 100644 index 0000000..27f299c --- /dev/null +++ b/src/common/IconPicker.tsx @@ -0,0 +1,230 @@ +import { + Avatar, + CircularProgress, + TextField, + Theme, + Typography, + useTheme +} from "@mui/material"; +import React, { useState } from "react"; +import { useImagekitAPI } from "providers"; +import { SupervisedUserCircle as TeamIcon } from "@mui/icons-material"; +import { makeStyles } from "@mui/styles"; + +const defaults = [ + "", + "https://ik.imagekit.io/adaptive/defaults/bird_ty8BTljNA.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663103520016", + "https://ik.imagekit.io/adaptive/defaults/leaf_rF3z5r2tZ.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212283945", + "https://ik.imagekit.io/adaptive/defaults/flower_Uc7QzEQs4R.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663214712629", + "https://ik.imagekit.io/adaptive/defaults/snowleopard_KbnacTZMB.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663103172262", + "https://ik.imagekit.io/adaptive/defaults/car_r5hNMcv7Y.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212283392", + "https://ik.imagekit.io/adaptive/defaults/sunflower_Pg8sac7NK.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663214713051", + "https://ik.imagekit.io/adaptive/defaults/deer__O5DKbszp.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663102676111", + "https://ik.imagekit.io/adaptive/defaults/polarbear_5PMEyQvwH.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208045638", + "https://ik.imagekit.io/adaptive/defaults/beach_i2CM4_uB3.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212283346", + "https://ik.imagekit.io/adaptive/defaults/bunny_ofnih4I6k.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208045524", + "https://ik.imagekit.io/adaptive/defaults/fox_4CPQjzeSw.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208044904", + "https://ik.imagekit.io/adaptive/defaults/frog_An4L9CIIj.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208044711", + "https://ik.imagekit.io/adaptive/defaults/lighthouse_U56RsSQws.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663212280873", + "https://ik.imagekit.io/adaptive/defaults/ball_fpYXIQarg.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663214713217", + "https://ik.imagekit.io/adaptive/defaults/dog_bpolRB5HP.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663208041090", + "https://ik.imagekit.io/adaptive/defaults/turtle__kcJnFkt1.jpg?ik-sdk-version=javascript-1.4.3&updatedAt=1663207995148" +]; + +const useStyles = makeStyles((theme: Theme) => ({ + icon: { + textAlign: "center", + margin: "1px" + }, + img: { + height: "100%", + width: "auto !important" + }, + avatar: { + color: "#fff", + [theme.breakpoints.down("xs")]: { + width: "32px", + height: "32px" + }, + [theme.breakpoints.down("lg")]: { + width: "64px", + height: "64px" + } + }, + avatarBig: { + [theme.breakpoints.down("lg")]: { + width: "128px", + height: "128px" + } + }, + selected: { + backgroundColor: "rgba(150, 150, 150, 0.25)" + } +})); + +interface Props { + url: string; + setUrl: React.Dispatch>; + id: string; + style?: any; + title?: string; +} + +export function IconPicker(props: Props) { + const { setUrl, id, title, url } = props; + const classes = useStyles(); + const theme = useTheme(); + const imagekitAPI = useImagekitAPI(); + + const [loading, setLoading] = useState(false); + + const onChangeFile = (event: any) => { + event.stopPropagation(); + event.preventDefault(); + var file = event.target.files[0]; + setLoading(true); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = event => { + resolve(event?.target?.result); + }; + reader.onerror = err => { + reject(err); + }; + reader.readAsDataURL(file); + }).then((resp: any) => { + let r: string = resp.toString(); + imagekitAPI + .uploadImage("icon", r, "/" + id + "/") + .then(resp => { + setUrl(resp.data.url + "?v=" + Math.random()); + setLoading(false); + }) + .catch(err => { + console.log(err); + setLoading(false); + }); + }); + }; + + const top = () => { + return ( +
+
+ {title ? title : "Icon"} + + + +
+ {sources()} +
+ ); + }; + + const picker = () => { + return ( +
+ {defaults.map((src, index) => { + return ( +
{ + setUrl(src); + }} + className={url === src ? classes.selected : undefined}> + + + +
+ ); + })} +
+ ); + }; + + const sources = () => { + return ( +
+ onChangeFile(event)} + style={{ + margin: theme.spacing(1) + }} + /> + {loading && ( + + )} + { + setUrl(event.currentTarget.value); + }} + /> +
+ ); + }; + + return ( + + {top()} + + Pick one + + {picker()} + + ); +} diff --git a/src/common/NotificationButton.tsx b/src/common/NotificationButton.tsx new file mode 100644 index 0000000..aee4f56 --- /dev/null +++ b/src/common/NotificationButton.tsx @@ -0,0 +1,41 @@ +import { createStyles, IconButton, Tooltip } from "@mui/material"; +// import { Theme } from "@material-ui/core/styles/createMuiTheme"; +// import withStyles, { WithStyles } from "@material-ui/core/styles/withStyles"; +import NotificationsEnabledIcon from "@mui/icons-material/NotificationsActive"; +import NotificationsDisabledIcon from "@mui/icons-material/NotificationsOff"; +import { WithStyles } from "@mui/styles"; + +const styles = () => createStyles({}); + +interface Props extends WithStyles { + notify: boolean; + hidden?: boolean; + onChange: Function; + disabled?: boolean; + tooltip: string; +} + +// interface State {} + +export default function NotificationButton (props: Props) { + + const { notify, onChange, hidden, tooltip, disabled } = props; + + if (hidden) { + return null; + } + + return ( + + + onChange()} + disabled={disabled}> + {notify ? : } + + + + ); +} + diff --git a/src/common/ResponsiveDialog.tsx b/src/common/ResponsiveDialog.tsx new file mode 100644 index 0000000..c974ca6 --- /dev/null +++ b/src/common/ResponsiveDialog.tsx @@ -0,0 +1,38 @@ +import { Dialog, Theme } from "@mui/material"; +import { DialogProps } from "@mui/material/Dialog"; +// import { TransitionProps } from "@mui/material/transitions"; +import { makeStyles } from "@mui/styles"; +import { useMobile } from "hooks"; +// import React from "react"; + +const useStyles = makeStyles((theme: Theme) => ({ + bg: { + background: theme.palette.background.default + } +})); + +// const Transition = React.forwardRef(function Transition( +// props: TransitionProps & { children?: React.ReactElement }, +// ref: React.Ref +// ) { +// return ; +// }); + +//renders a fullscren dialog when a mobile viewport is detected +export default function ResponsiveDialog(props: DialogProps) { + const classes = useStyles(); + const isMobile = useMobile(); + const fullScreen = props.fullScreen ?? isMobile; + + return ( + + {props.children} + + ); +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 02fe85e..4f5b58b 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -15,18 +15,18 @@ export { // useMeasurementsWebsocket, // useMetricAPI, // useMineAPI, -// usePermissionAPI, + usePermissionAPI, // usePreferenceAPI, // useSecurity, -// useSnackbar, + useSnackbar, // useTagAPI, // useUsageAPI, -// useUserAPI + useUserAPI } from "providers"; // export * from "./useDebounce"; // export * from "./useForceUpdate"; // export * from "./useInterval"; -// export * from "./usePrevious"; +export * from "./usePrevious"; export * from "./useThemeType"; export * from "./useWidth"; // export * from "./useViewport"; \ No newline at end of file diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts new file mode 100644 index 0000000..2ac35f1 --- /dev/null +++ b/src/hooks/usePrevious.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from "react"; + +export const usePrevious = (value: T) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; diff --git a/src/models/ShareableLink.ts b/src/models/ShareableLink.ts new file mode 100644 index 0000000..b4f9202 --- /dev/null +++ b/src/models/ShareableLink.ts @@ -0,0 +1,32 @@ +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; +import { cloneDeep } from "lodash"; + +export class ShareableLink { + public settings: pond.ShareableLinkSettings = pond.ShareableLinkSettings.create(); + public status: pond.ShareableLinkStatus = pond.ShareableLinkStatus.create(); + + public static create(pb?: pond.ShareableLink): ShareableLink { + let my = new ShareableLink(); + if (pb) { + my.settings = pond.ShareableLinkSettings.fromObject(cloneDeep(or(pb.settings, {}))); + my.status = pond.ShareableLinkStatus.fromObject(cloneDeep(or(pb.status, {}))); + } + return my; + } + + public static any(data: any): ShareableLink { + return ShareableLink.create(pond.ShareableLink.fromObject(cloneDeep(data))); + } + + public static clone(link: ShareableLink): ShareableLink { + let my = new ShareableLink(); + my.settings = pond.ShareableLinkSettings.fromObject(cloneDeep(link.settings)); + my.status = pond.ShareableLinkStatus.fromObject(cloneDeep(link.status)); + return my; + } + + public key(): string { + return this.settings.key; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 9b28458..ee21f45 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -7,7 +7,7 @@ // export * from "./Group"; // export * from "./Interaction"; export * from "./Scope"; -// export * from "./ShareableLink"; +export * from "./ShareableLink"; // export * from "./Site"; // export * from "./Tag"; // export * from "./Task"; diff --git a/src/pages/Teams.tsx b/src/pages/Teams.tsx index a47be04..aae8b62 100644 --- a/src/pages/Teams.tsx +++ b/src/pages/Teams.tsx @@ -1,15 +1,15 @@ import { Box } from "@mui/material"; import { useMobile } from "hooks"; import PageContainer from "pages/PageContainer"; -// import TeamList from "teams/TeamList"; +import TeamList from "teams/TeamList"; export default function Teams() { const isMobile = useMobile(); return ( - {/* */} - Teams! + + {/* Teams! */} ); diff --git a/src/pbHelpers/Context.ts b/src/pbHelpers/Context.ts new file mode 100644 index 0000000..e69ccde --- /dev/null +++ b/src/pbHelpers/Context.ts @@ -0,0 +1,27 @@ +export function getContextKeys(): string[] { + // Get the url entries without any zero length strings + let entries = document.location.pathname.split("/").filter(key => key.length > 0); + let cutTail = entries.length % 2 === 0; + + // Filter out every other entry + entries = entries.filter((_, index) => index % 2 === 1); + + if (cutTail) return entries.slice(0, -1); + + return entries; +} + +export function getContextTypes(): string[] { + // Get the url entries without any zero length strings + let entries = document.location.pathname.split("/").filter(key => key.length > 0); + + // Filter out every other entry + entries = entries.filter((_, index) => index % 2 === 0); + + // jobsite should just be site and none should end in s anymore + entries = entries.slice(0, -1).map(str => (str.endsWith("s") ? str.slice(0, -1) : str)); + entries = entries.map(item => (item === "jobsite" ? "site" : item)); + + // Return all types but the last (it is the object being loaded, not the context) + return entries; +} diff --git a/src/pbHelpers/Permission.ts b/src/pbHelpers/Permission.ts new file mode 100644 index 0000000..23a3b98 --- /dev/null +++ b/src/pbHelpers/Permission.ts @@ -0,0 +1,21 @@ +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; + +export function permissionToString(permission: pond.Permission): string { + return or( + new Map([ + [pond.Permission.PERMISSION_INVALID, "PERMISSION_INVALID"], + [pond.Permission.PERMISSION_READ, "PERMISSION_READ"], + [pond.Permission.PERMISSION_SHARE, "PERMISSION_SHARE"], + [pond.Permission.PERMISSION_USERS, "PERMISSION_USERS"], + [pond.Permission.PERMISSION_WRITE, "PERMISSION_WRITE"], + [pond.Permission.PERMISSION_FILE_MANAGEMENT, "PERMISSION_FILE_MANAGEMENT"], + [pond.Permission.PERMISSION_BILLING, "PERMISSION_BILLING"] + ]).get(permission), + "PERMISSION_INVALID" + ); +} + +export function canWrite(permissions: pond.Permission[]): boolean { + return permissions.includes(pond.Permission.PERMISSION_WRITE); +} diff --git a/src/pbHelpers/User.ts b/src/pbHelpers/User.ts new file mode 100644 index 0000000..fb0bb49 --- /dev/null +++ b/src/pbHelpers/User.ts @@ -0,0 +1,41 @@ +import { pond } from "protobuf-ts/pond"; +import moment from "moment-timezone"; +import { User } from "models"; + +export function userRoleFromPermissions(permissions: Array): string { + //only be an owner if they have all 5 + if ( + permissions.includes(pond.Permission.PERMISSION_USERS) && + permissions.includes(pond.Permission.PERMISSION_READ) && + permissions.includes(pond.Permission.PERMISSION_WRITE) && + permissions.includes(pond.Permission.PERMISSION_SHARE) && + permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) + ) + return "Owner"; + if (permissions.includes(pond.Permission.PERMISSION_WRITE)) return "Admin"; + return "User"; +} + +export function sortUsersByRole(users: User[]): User[] { + let sortedUsers: User[] = users.slice(0).sort((a: User, b: User) => { + if (a.permissions.length === b.permissions.length) { + let aName: string = a.name().toLowerCase(); + let bName: string = b.name().toLowerCase(); + return aName > bName ? 1 : bName > aName ? -1 : 0; + } + return a.permissions.length - b.permissions.length; + }); + + return sortedUsers; +} + +export function defaultUser(): User { + return User.any({ + settings: { + notifyByDefault: true, + notificationMethods: [pond.NotificationMethod.NOTIFICATION_METHOD_EMAIL], + phoneNumber: "", + timezone: moment.tz.guess() + } + }); +} diff --git a/src/products/AgIcons/AddField.tsx b/src/products/AgIcons/AddField.tsx new file mode 100644 index 0000000..4b33546 --- /dev/null +++ b/src/products/AgIcons/AddField.tsx @@ -0,0 +1,24 @@ +import AddFieldWhite from "assets/products/Ag/addFieldWhite.png"; +import AddFieldBlack from "assets/products/Ag/addFieldBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function AddFieldIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? AddFieldWhite : AddFieldBlack; + } + + return themeType === "light" ? AddFieldBlack : AddFieldWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/AddMarker.tsx b/src/products/AgIcons/AddMarker.tsx new file mode 100644 index 0000000..e15dca3 --- /dev/null +++ b/src/products/AgIcons/AddMarker.tsx @@ -0,0 +1,24 @@ +import AddMarkerWhite from "assets/products/Ag/addMarkerWhite.png"; +import AddMarkerBlack from "assets/products/Ag/addMarkerBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function AddMarkerIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? AddMarkerWhite : AddMarkerBlack; + } + + return themeType === "light" ? AddMarkerBlack : AddMarkerWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/AerationFanIcon.tsx b/src/products/AgIcons/AerationFanIcon.tsx new file mode 100644 index 0000000..b208e38 --- /dev/null +++ b/src/products/AgIcons/AerationFanIcon.tsx @@ -0,0 +1,29 @@ +import AerationFanDarkIcon from "assets/components/aerationFanDark.png"; +import AerationFanLightIcon from "assets/components/aerationFanLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; + size?: number; +} + +export default function AerationFanIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? AerationFanLightIcon : AerationFanDarkIcon; + } + + return themeType === "light" ? AerationFanDarkIcon : AerationFanLightIcon; + }; + + if (size) { + return fan; + } + + return ; +} diff --git a/src/products/AgIcons/Delete.tsx b/src/products/AgIcons/Delete.tsx new file mode 100644 index 0000000..7c86184 --- /dev/null +++ b/src/products/AgIcons/Delete.tsx @@ -0,0 +1,24 @@ +import DeleteWhite from "assets/products/Ag/deleteWhite.png"; +import DeleteBlack from "assets/products/Ag/deleteBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function DeleteIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? DeleteWhite : DeleteBlack; + } + + return themeType === "light" ? DeleteBlack : DeleteWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/DiseaseIcon.tsx b/src/products/AgIcons/DiseaseIcon.tsx new file mode 100644 index 0000000..ac56b53 --- /dev/null +++ b/src/products/AgIcons/DiseaseIcon.tsx @@ -0,0 +1,25 @@ +import DiseaseLight from "assets/products/Ag/diseaseLight.png"; +import DiseaseDark from "assets/products/Ag/diseaseDark.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width: number; + height: number; + type?: "light" | "dark"; +} + +export default function DiseaseIcon(props: Props) { + const themeType = useThemeType(); + const { width, height, type } = props; + + const src = () => { + if (type) { + return type === "light" ? DiseaseLight : DiseaseDark; + } + + return themeType === "light" ? DiseaseDark : DiseaseLight; + }; + + return diseaseIcon; +} diff --git a/src/products/AgIcons/Edit.tsx b/src/products/AgIcons/Edit.tsx new file mode 100644 index 0000000..b359dd5 --- /dev/null +++ b/src/products/AgIcons/Edit.tsx @@ -0,0 +1,24 @@ +import EditWhite from "assets/products/Ag/editWhite.png"; +import EditBlack from "assets/products/Ag/editBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function EditIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? EditWhite : EditBlack; + } + + return themeType === "light" ? EditBlack : EditWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/FieldList.tsx b/src/products/AgIcons/FieldList.tsx new file mode 100644 index 0000000..d87d10f --- /dev/null +++ b/src/products/AgIcons/FieldList.tsx @@ -0,0 +1,24 @@ +import FieldListWhite from "assets/products/Ag/fieldListWhite.png"; +import FieldListBlack from "assets/products/Ag/fieldListBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function FieldListIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? FieldListWhite : FieldListBlack; + } + + return themeType === "light" ? FieldListBlack : FieldListWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/FieldMap.tsx b/src/products/AgIcons/FieldMap.tsx new file mode 100644 index 0000000..41988c0 --- /dev/null +++ b/src/products/AgIcons/FieldMap.tsx @@ -0,0 +1,24 @@ +import FieldMapWhite from "assets/products/Ag/fieldMapWhite.png"; +import FieldMapBlack from "assets/products/Ag/fieldMapBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function FieldMapIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? FieldMapWhite : FieldMapBlack; + } + + return themeType === "light" ? FieldMapBlack : FieldMapWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/FieldNames.tsx b/src/products/AgIcons/FieldNames.tsx new file mode 100644 index 0000000..49e7e07 --- /dev/null +++ b/src/products/AgIcons/FieldNames.tsx @@ -0,0 +1,24 @@ +import FieldNamesWhite from "assets/products/Ag/fieldNamesWhite.png"; +import FieldNamesBlack from "assets/products/Ag/fieldNamesBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function FieldNamesIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? FieldNamesWhite : FieldNamesBlack; + } + + return themeType === "light" ? FieldNamesBlack : FieldNamesWhite; + }; + + return ; +} diff --git a/src/products/AgIcons/FieldsIcon.tsx b/src/products/AgIcons/FieldsIcon.tsx new file mode 100644 index 0000000..e5c80fc --- /dev/null +++ b/src/products/AgIcons/FieldsIcon.tsx @@ -0,0 +1,29 @@ +import FieldsIconLight from "assets/products/Ag/FieldsIconLight.png"; +import FieldsIconDark from "assets/products/Ag/FieldsIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; + size?: number; +} + +export default function FieldsIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? FieldsIconLight : FieldsIconDark; + } + + return themeType === "light" ? FieldsIconDark : FieldsIconLight; + }; + + return size ? ( + fields + ) : ( + + ); +} diff --git a/src/products/AgIcons/GearMarker.tsx b/src/products/AgIcons/GearMarker.tsx new file mode 100644 index 0000000..f83f7da --- /dev/null +++ b/src/products/AgIcons/GearMarker.tsx @@ -0,0 +1,24 @@ +import GearMarkerLight from "assets/products/Ag/gearMarkerLight.png"; +import GearMarkerDark from "assets/products/Ag/gearMarkerDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function GearMarkerIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? GearMarkerLight : GearMarkerDark; + } + + return themeType === "light" ? GearMarkerDark : GearMarkerLight; + }; + + return ; +} diff --git a/src/products/AgIcons/HomeIcon.tsx b/src/products/AgIcons/HomeIcon.tsx new file mode 100644 index 0000000..d3dd562 --- /dev/null +++ b/src/products/AgIcons/HomeIcon.tsx @@ -0,0 +1,24 @@ +import HomeIconLight from "assets/products/Ag/HomeIconLight.png"; +import HomeIconDark from "assets/products/Ag/HomeIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function HomeIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? HomeIconLight : HomeIconDark; + } + + return themeType === "light" ? HomeIconDark : HomeIconLight; + }; + + return ; +} diff --git a/src/products/AgIcons/MarkerMove.tsx b/src/products/AgIcons/MarkerMove.tsx new file mode 100644 index 0000000..7e5f869 --- /dev/null +++ b/src/products/AgIcons/MarkerMove.tsx @@ -0,0 +1,24 @@ +import MarkerMoveLight from "assets/products/Ag/markerMoveLight.png"; +import MarkerMoveDark from "assets/products/Ag/markerMoveDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function MarkerMoveIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? MarkerMoveLight : MarkerMoveDark; + } + + return themeType === "light" ? MarkerMoveDark : MarkerMoveLight; + }; + + return ; +} diff --git a/src/products/AgIcons/PestsIcon.tsx b/src/products/AgIcons/PestsIcon.tsx new file mode 100644 index 0000000..1fa96c3 --- /dev/null +++ b/src/products/AgIcons/PestsIcon.tsx @@ -0,0 +1,25 @@ +import PestsLight from "assets/products/Ag/pestsLight.png"; +import PestsDark from "assets/products/Ag/pestsDark.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width: number; + height: number; + type?: "light" | "dark"; +} + +export default function PestsIcon(props: Props) { + const themeType = useThemeType(); + const { width, height, type } = props; + + const src = () => { + if (type) { + return type === "light" ? PestsLight : PestsDark; + } + + return themeType === "light" ? PestsDark : PestsLight; + }; + + return pestsIcon; +} diff --git a/src/products/AgIcons/PressureIcon.tsx b/src/products/AgIcons/PressureIcon.tsx new file mode 100644 index 0000000..21296f8 --- /dev/null +++ b/src/products/AgIcons/PressureIcon.tsx @@ -0,0 +1,37 @@ +import PressureDarkIcon from "assets/components/pressureDark.png"; +import PressureLightIcon from "assets/components/pressureLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; + heightWidth?: number; +} + +export default function TemperatureIcon(props: Props) { + const themeType = useThemeType(); + const { type, heightWidth } = props; + + const lightIcon = () => { + return PressureLightIcon; + }; + + const darkIcon = () => { + return PressureDarkIcon; + }; + + const src = () => { + if (type) { + return type === "light" ? lightIcon() : darkIcon(); + } + + return themeType === "light" ? darkIcon() : lightIcon(); + }; + + if (heightWidth) { + return pressure; + } + + return ; +} diff --git a/src/products/AgIcons/RocksIcon.tsx b/src/products/AgIcons/RocksIcon.tsx new file mode 100644 index 0000000..183b87b --- /dev/null +++ b/src/products/AgIcons/RocksIcon.tsx @@ -0,0 +1,25 @@ +import RocksLight from "assets/products/Ag/rocksLight.png"; +import RocksDark from "assets/products/Ag/rocksDark.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width: number; + height: number; + type?: "light" | "dark"; +} + +export default function RocksIcon(props: Props) { + const themeType = useThemeType(); + const { width, height, type } = props; + + const src = () => { + if (type) { + return type === "light" ? RocksLight : RocksDark; + } + + return themeType === "light" ? RocksDark : RocksLight; + }; + + return rocksIcon; +} diff --git a/src/products/AgIcons/ScoutIcon.tsx b/src/products/AgIcons/ScoutIcon.tsx new file mode 100644 index 0000000..158977e --- /dev/null +++ b/src/products/AgIcons/ScoutIcon.tsx @@ -0,0 +1,24 @@ +import ScoutLight from "assets/products/Ag/scoutLight.png"; +import ScoutDark from "assets/products/Ag/scoutDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function ScoutIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? ScoutLight : ScoutDark; + } + + return themeType === "light" ? ScoutDark : ScoutLight; + }; + + return ; +} diff --git a/src/products/AgIcons/WeedsIcon.tsx b/src/products/AgIcons/WeedsIcon.tsx new file mode 100644 index 0000000..82b9f1f --- /dev/null +++ b/src/products/AgIcons/WeedsIcon.tsx @@ -0,0 +1,25 @@ +import WeedsLight from "assets/products/Ag/weedsLight.png"; +import WeedsDark from "assets/products/Ag/weedsDark.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width: number; + height: number; + type?: "light" | "dark"; +} + +export default function WeedsIcon(props: Props) { + const themeType = useThemeType(); + const { width, height, type } = props; + + const src = () => { + if (type) { + return type === "light" ? WeedsLight : WeedsDark; + } + + return themeType === "light" ? WeedsDark : WeedsLight; + }; + + return weedsIcon; +} diff --git a/src/products/AviationIcons/AddPlaneIcon.tsx b/src/products/AviationIcons/AddPlaneIcon.tsx new file mode 100644 index 0000000..d89ba4b --- /dev/null +++ b/src/products/AviationIcons/AddPlaneIcon.tsx @@ -0,0 +1,25 @@ +import White from "assets/products/Aviation/AddPlaneIconWhite.png"; +import Black from "assets/products/Aviation/AddPlaneIconBlack.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width?: number; + height?: number; + type?: "light" | "dark"; +} + +export default function AddPlaneIcon(props: Props) { + const themeType = useThemeType(); + const { type, width, height } = props; + + const src = () => { + if (type) { + return type === "light" ? White : Black; + } + + return themeType === "light" ? Black : White; + }; + + return addPlane; +} diff --git a/src/products/AviationIcons/AirportMapIcon.tsx b/src/products/AviationIcons/AirportMapIcon.tsx new file mode 100644 index 0000000..91e30f0 --- /dev/null +++ b/src/products/AviationIcons/AirportMapIcon.tsx @@ -0,0 +1,25 @@ +import White from "assets/products/Aviation/AirportMapWhite.png"; +import Black from "assets/products/Aviation/AirportMapBlack.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width?: number; + height?: number; + type?: "light" | "dark"; +} + +export default function AirportMapIcon(props: Props) { + const themeType = useThemeType(); + const { type, width, height } = props; + + const src = () => { + if (type) { + return type === "light" ? White : Black; + } + + return themeType === "light" ? Black : White; + }; + + return airportMap; +} diff --git a/src/products/AviationIcons/OmniAirDeviceIcon.tsx b/src/products/AviationIcons/OmniAirDeviceIcon.tsx new file mode 100644 index 0000000..6101bc5 --- /dev/null +++ b/src/products/AviationIcons/OmniAirDeviceIcon.tsx @@ -0,0 +1,24 @@ +import White from "assets/products/Aviation/OmniAirDeviceIconWhite.png"; +import Black from "assets/products/Aviation/OmniAirDeviceIconBlack.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + size?: number; + type?: "light" | "dark"; +} + +export default function OmniAirDeviceIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? White : Black; + } + + return themeType === "light" ? Black : White; + }; + + return OmniAirDevice; +} diff --git a/src/products/AviationIcons/PlaneIcon.tsx b/src/products/AviationIcons/PlaneIcon.tsx new file mode 100644 index 0000000..0abce7d --- /dev/null +++ b/src/products/AviationIcons/PlaneIcon.tsx @@ -0,0 +1,25 @@ +import White from "assets/products/Aviation/PlaneWhite.png"; +import Black from "assets/products/Aviation/PlaneBlack.png"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + width?: number; + height?: number; + type?: "light" | "dark"; +} + +export default function PlaneIcon(props: Props) { + const themeType = useThemeType(); + const { type, width, height } = props; + + const src = () => { + if (type) { + return type === "light" ? White : Black; + } + + return themeType === "light" ? Black : White; + }; + + return plane; +} diff --git a/src/products/Bindapt/AddBinIcon.tsx b/src/products/Bindapt/AddBinIcon.tsx new file mode 100644 index 0000000..85db887 --- /dev/null +++ b/src/products/Bindapt/AddBinIcon.tsx @@ -0,0 +1,24 @@ +import AddBinDark from "assets/products/bindapt/addBin.png"; +import AddBinLight from "assets/products/bindapt/addBinLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function AddBinIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? AddBinLight : AddBinDark; + } + + return themeType === "light" ? AddBinDark : AddBinLight; + }; + + return ; +} diff --git a/src/products/Bindapt/BindaptAvailability.ts b/src/products/Bindapt/BindaptAvailability.ts new file mode 100644 index 0000000..73b5d50 --- /dev/null +++ b/src/products/Bindapt/BindaptAvailability.ts @@ -0,0 +1,292 @@ +import { ConfigurablePin } from "pbHelpers/AddressTypes"; +import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability"; +import { quack } from "protobuf-ts/pond"; + +const BindaptPlusPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 512, label: "C1" } +]; + +export const BindaptPlusAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptPlusModPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 512, label: "C1" } +]; + +export const BindaptPlusModAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusModPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + //[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptPlusProPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 512, label: "C1" }, + { address: 1024, label: "C2" } +]; + +export const BindaptPlusProAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusProPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptPlusProModPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 32, label: "4" }, + { address: 512, label: "C1" }, + { address: 1024, label: "C2" } +]; + +export const BindaptPlusProModAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusProModPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + //[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptPlusLitePins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" } +]; + +export const BindaptPlusLiteAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptPlusLitePins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptMiniPins: ConfigurablePin[] = [ + { address: 256, label: "1" }, + { address: 512, label: "2" } +]; + +export const BindaptMiniAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptMiniPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([[quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]]]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptUltimatePins: ConfigurablePin[] = [ + { address: 1, label: "P1" }, + { address: 4, label: "P2" }, + { address: 8, label: "P3" }, + { address: 8192, label: "P4" }, + { address: 16, label: "P5" }, + //{ address: 32, label: "P6"}, //for possible expansion if we want a sixth input port on the box + { address: 128, label: "C1" }, + { address: 256, label: "C2" }, + { address: 512, label: "C3" }, + { address: 1024, label: "C4" } + //{ address: 2048, label: "C5"} //for possible expansion if we want a fifth control port on the box +]; + +export const BindaptUltimateAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptUltimatePins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BinMonitorPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" } +]; + +export const BinMonitorAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BinMonitorPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +/** V2 product availability maps */ + +//TODO: change these pins to match the V2 pin map +const BindaptV2MonitorPins: ConfigurablePin[] = [ + { address: 1, label: "1" }, + { address: 2, label: "2" } +]; + +export const BindaptV2MonitorAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptV2MonitorPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + //[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +const BindaptV2AutomatePins: ConfigurablePin[] = [ + { address: 1, label: "1" }, + { address: 2, label: "2" }, + { address: 4, label: "3" }, + { address: 8, label: "4" }, + { address: 256, label: "C1" }, + { address: 512, label: "C2" }, + { address: 1024, label: "C3" } +]; + +export const BindaptV2AutomateAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, BindaptV2AutomatePins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + //[quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); diff --git a/src/products/Bindapt/BindaptDescriber.ts b/src/products/Bindapt/BindaptDescriber.ts new file mode 100644 index 0000000..2bb4b63 --- /dev/null +++ b/src/products/Bindapt/BindaptDescriber.ts @@ -0,0 +1,210 @@ +import BindaptCellularDarkIcon from "assets/products/bindapt/bindaptCellularDark.png"; +import BindaptCellularLightIcon from "assets/products/bindapt/bindaptCellularLight.png"; +import BindaptPlusDarkIcon from "assets/products/bindapt/bindaptPlusDark.png"; +import BindaptPlusLightIcon from "assets/products/bindapt/bindaptPlusLight.png"; +import BindaptPlusProDarkIcon from "assets/products/bindapt/bindaptPlusProDark.png"; +import BindaptPlusProLightIcon from "assets/products/bindapt/bindaptPlusProLight.png"; +import BindaptWifiDarkIcon from "assets/products/bindapt/bindaptWifiDark.png"; +import BindaptWifiLightIcon from "assets/products/bindapt/bindaptWifiLight.png"; +import { DeviceProductDescriber, ProductTab } from "products/DeviceProduct"; +import { pond } from "protobuf-ts/pond"; +import { + BindaptPlusAvailability, + BindaptPlusProAvailability, + BindaptPlusLiteAvailability, + BindaptMiniAvailability, + BindaptUltimateAvailability, + BinMonitorAvailability, + BindaptPlusModAvailability, + BindaptPlusProModAvailability, + BindaptV2MonitorAvailability, + BindaptV2AutomateAvailability +} from "./BindaptAvailability"; + +const getBindaptPlatformIcon = ( + platform: pond.DevicePlatform, + theme?: "light" | "dark" +): string | undefined => { + switch (platform) { + case pond.DevicePlatform.DEVICE_PLATFORM_PHOTON: + return theme === "light" ? BindaptWifiDarkIcon : BindaptWifiLightIcon; + case pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON: + return theme === "light" ? BindaptCellularDarkIcon : BindaptCellularLightIcon; + default: + return undefined; + } +}; + +const getBindaptTabs = (): ProductTab[] => { + return ["presets", "components"]; +}; + +export const BindaptPlus: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS, + label: "Bindapt+", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptPlusAvailability, + defaultAddressMap: new Map([ + ["plenum", ["9-2-64"]], + ["pressure", ["6-2-24"]], + ["heatersFans", ["3-1-512"]] //refers to heaters and fans + ]) +}; + +export const BindaptPlusMod: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD, + label: "Bindapt+ V1.5", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptPlusModAvailability + //TODO not sure what the default address map for the modded products should be yet + // defaultAddressMap: new Map([ + // ["plenum", ["9-2-64"]], + // ["heatersFans", ["3-1-512"]] //refers to heaters and fans + // ]) +}; + +export const BindaptPlusPro: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO, + label: "Bindapt+ Pro", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusProDarkIcon : BindaptPlusProLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptPlusProAvailability, + defaultAddressMap: new Map([ + ["plenum", ["9-2-64"]], + ["pressure", ["6-2-24"]], + ["heatersFans", ["3-1-512", "3-1-1024"]] //refers to heaters and fans + ]) +}; + +export const BindaptPlusProMod: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD, + label: "Bindapt+ Pro V1.5", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusProDarkIcon : BindaptPlusProLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptPlusProModAvailability + //TODO not sure what the default address map should be yet + // defaultAddressMap: new Map([ + // ["plenum", ["9-2-64"]], + // ["heatersFans", ["3-1-512", "3-1-1024"]] //refers to heaters and fans + // ]) +}; + +export const BindaptPlusLite: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_LITE, + label: "Bindapt+ Lite", + icon: getBindaptPlatformIcon, + tabs: getBindaptTabs(), + availability: BindaptPlusLiteAvailability +}; + +export const BindaptMini: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI, + label: "Bindapt Mini", + icon: getBindaptPlatformIcon, + tabs: getBindaptTabs(), + availability: BindaptMiniAvailability +}; + +export const BinUltimate: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BIN_ULTIMATE, + label: "Bindapt Ultimate", + icon: getBindaptPlatformIcon, + tabs: getBindaptTabs(), + view: "controller", + availability: BindaptUltimateAvailability, + defaultAddressMap: new Map([ + ["headspace", ["9-2-64", "28-2-98"]], //is both the dht and lidar components in the halo + ["pressure", ["6-2-24"]], + ["heatersFans", ["3-1-512", "3-1-1024", "3-1-128", "3-1-256"]] //refers to heaters and fans + ]) +}; + +export const BinMonitor: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BIN_MONITOR, + label: "Bin Monitor", + icon: getBindaptPlatformIcon, + tabs: getBindaptTabs(), + availability: BinMonitorAvailability, + defaultAddressMap: new Map([ + ["headspace", ["9-2-64", "28-2-98"]], //is both the dht and lidar components in the halo + ["pressure", ["6-2-24"]] + ]) +}; + +export const BinHalo: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BIN_HALO, + label: "Bin Halo", + icon: getBindaptPlatformIcon, + tabs: getBindaptTabs(), + view: "controller", + availability: BindaptPlusAvailability, + defaultAddressMap: new Map([ + ["headspace", ["9-2-64", "28-2-98"]], //is both the dht and lidar components in the halo + ["pressure", ["6-2-24"]], + ["heatersFans", ["3-1-512"]] //refers to heaters and fans + ]) +}; + +export const BindaptV2Monitor: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR, + label: "Bindapt+ V2 Monitor", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptV2MonitorAvailability + //TODO not sure what the default address map for these should be yet + // defaultAddressMap: new Map([ + // ["plenum", ["9-2-64"]], + // ["heatersFans", ["3-1-512"]] //refers to heaters and fans + // ]) +}; + +export const BindaptV2Automate: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE, + label: "Bindapt+ V2 Automate", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? BindaptPlusDarkIcon : BindaptPlusLightIcon; + }, + view: "controller", + tabs: getBindaptTabs(), + availability: BindaptV2AutomateAvailability + //TODO not sure what the default address map for these should be yet + // defaultAddressMap: new Map([ + // ["plenum", ["9-2-64"]], + // ["heatersFans", ["3-1-512"]] //refers to heaters and fans + // ]) +}; + +export function IsBindaptDevice(product?: pond.DeviceProduct): boolean { + const bindaptProducts: pond.DeviceProduct[] = [ + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_LITE, + pond.DeviceProduct.DEVICE_PRODUCT_BIN_ULTIMATE, + pond.DeviceProduct.DEVICE_PRODUCT_BIN_MONITOR, + pond.DeviceProduct.DEVICE_PRODUCT_BIN_HALO, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR, + pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE + ]; + + return product ? bindaptProducts.includes(product) : false; +} diff --git a/src/products/Bindapt/BinsIcon.tsx b/src/products/Bindapt/BinsIcon.tsx new file mode 100644 index 0000000..28ada08 --- /dev/null +++ b/src/products/Bindapt/BinsIcon.tsx @@ -0,0 +1,39 @@ +import BinDarkIcon from "../../assets/products/bindapt/binDark.png"; +import BinLightIcon from "../../assets/products/bindapt/binLight.png"; +import BinsDarkIcon from "assets/products/bindapt/binsDark.png"; +import BinsLightIcon from "assets/products/bindapt/binsLight.png"; +import { ImgIcon } from "../../common/ImgIcon"; +import { useThemeType } from "../../hooks/useThemeType"; + +interface Props { + singleBin?: boolean; + type?: "light" | "dark"; + size?: number; +} + +export default function BinsIcon(props: Props) { + const themeType = useThemeType(); + const { type, singleBin, size } = props; + + const lightIcon = () => { + return singleBin ? BinLightIcon : BinsLightIcon; + }; + + const darkIcon = () => { + return singleBin ? BinDarkIcon : BinsDarkIcon; + }; + + const src = () => { + if (type) { + return type === "light" ? lightIcon() : darkIcon(); + } + + return themeType === "light" ? darkIcon() : lightIcon(); + }; + + if (size) { + return bins; + } + + return ; +} diff --git a/src/products/Bindapt/CableIcon.tsx b/src/products/Bindapt/CableIcon.tsx new file mode 100644 index 0000000..905bfab --- /dev/null +++ b/src/products/Bindapt/CableIcon.tsx @@ -0,0 +1,24 @@ +import GrainCableDarkIcon from "assets/components/grainCableDark.png"; +import GrainCableLightIcon from "assets/components/grainCableLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function CableIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? GrainCableLightIcon : GrainCableDarkIcon; + } + + return themeType === "light" ? GrainCableDarkIcon : GrainCableLightIcon; + }; + + return ; +} diff --git a/src/products/Bindapt/DataDuckIcon.tsx b/src/products/Bindapt/DataDuckIcon.tsx new file mode 100644 index 0000000..6a7dfe6 --- /dev/null +++ b/src/products/Bindapt/DataDuckIcon.tsx @@ -0,0 +1,24 @@ +import DataDuckDark from "assets/common/dataDuckDark.png"; +import DataDuckLight from "assets/common/dataDuckLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function DataDuckIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? DataDuckLight : DataDuckDark; + } + + return themeType === "light" ? DataDuckDark : DataDuckLight; + }; + + return ; +} diff --git a/src/products/Bindapt/constructionIcon.tsx b/src/products/Bindapt/constructionIcon.tsx new file mode 100644 index 0000000..0b9e100 --- /dev/null +++ b/src/products/Bindapt/constructionIcon.tsx @@ -0,0 +1,11 @@ +import ConstructionIconGold from "assets/products/bindapt/constructionIcon.png"; +import { ImgIcon } from "common/ImgIcon"; +import React from "react"; + +export default function ConstIcon() { + const src = () => { + return ConstructionIconGold; + }; + + return ; +} diff --git a/src/products/Bindapt/index.ts b/src/products/Bindapt/index.ts new file mode 100644 index 0000000..f926552 --- /dev/null +++ b/src/products/Bindapt/index.ts @@ -0,0 +1,4 @@ +export * from "./BindaptAvailability"; +export * from "./BindaptDescriber"; +export * from "./BindaptIcon"; +export * from "./BinsIcon"; diff --git a/src/products/CommonIcons/cnhiIcon.tsx b/src/products/CommonIcons/cnhiIcon.tsx new file mode 100644 index 0000000..a175305 --- /dev/null +++ b/src/products/CommonIcons/cnhiIcon.tsx @@ -0,0 +1,24 @@ +import CNHiLogoWhite from "assets/marketplaceImages/CNHiWhite.png"; +import CNHiLogoBlack from "assets/marketplaceImages/CNHiBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function CNHiIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? CNHiLogoWhite : CNHiLogoBlack; + } + + return themeType === "light" ? CNHiLogoBlack : CNHiLogoWhite; + }; + + return ; +} diff --git a/src/products/CommonIcons/contractIcon.tsx b/src/products/CommonIcons/contractIcon.tsx new file mode 100644 index 0000000..024288b --- /dev/null +++ b/src/products/CommonIcons/contractIcon.tsx @@ -0,0 +1,29 @@ +import ContractIconLight from "assets/common/contractWhite.png"; +import ContractIconDark from "assets/common/contractBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; + size?: number; +} + +export default function ContractsIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? ContractIconLight : ContractIconDark; + } + + return themeType === "light" ? ContractIconDark : ContractIconLight; + }; + + return size ? ( + fields + ) : ( + + ); +} diff --git a/src/products/CommonIcons/fuelIcon.tsx b/src/products/CommonIcons/fuelIcon.tsx new file mode 100644 index 0000000..fc469ab --- /dev/null +++ b/src/products/CommonIcons/fuelIcon.tsx @@ -0,0 +1,24 @@ +import FuelDark from "assets/components/fuelDark.png"; +import FuelLight from "assets/components/fuelLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function FuelIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? FuelLight : FuelDark; + } + + return themeType === "light" ? FuelDark : FuelLight; + }; + + return ; +} diff --git a/src/products/CommonIcons/graphIcon.tsx b/src/products/CommonIcons/graphIcon.tsx new file mode 100644 index 0000000..e21c4b5 --- /dev/null +++ b/src/products/CommonIcons/graphIcon.tsx @@ -0,0 +1,24 @@ +import graphDark from "assets/common/graphDark.png"; +import graphLight from "assets/common/graphLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function GraphIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? graphLight : graphDark; + } + + return themeType === "light" ? graphDark : graphLight; + }; + + return ; +} diff --git a/src/products/CommonIcons/johnDeereIcon.tsx b/src/products/CommonIcons/johnDeereIcon.tsx new file mode 100644 index 0000000..942fbb5 --- /dev/null +++ b/src/products/CommonIcons/johnDeereIcon.tsx @@ -0,0 +1,24 @@ +import JohnDeereLogoWhite from "assets/marketplaceImages/JDWhite.png"; +import JohnDeereLogoBlack from "assets/marketplaceImages/JDBlack.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function JohnDeereIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? JohnDeereLogoWhite : JohnDeereLogoBlack; + } + + return themeType === "light" ? JohnDeereLogoBlack : JohnDeereLogoWhite; + }; + + return ; +} diff --git a/src/products/CommonIcons/marketplaceIcon.tsx b/src/products/CommonIcons/marketplaceIcon.tsx new file mode 100644 index 0000000..20933b9 --- /dev/null +++ b/src/products/CommonIcons/marketplaceIcon.tsx @@ -0,0 +1,24 @@ +import MarketWhite from "assets/marketplaceImages/marketplaceIconLight.png"; +import MarketBlack from "assets/marketplaceImages/marketplaceIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function MarketplaceIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? MarketWhite : MarketBlack; + } + + return themeType === "light" ? MarketBlack : MarketWhite; + }; + + return ; +} diff --git a/src/products/Construction/ACHomeIcon.tsx b/src/products/Construction/ACHomeIcon.tsx new file mode 100644 index 0000000..783594e --- /dev/null +++ b/src/products/Construction/ACHomeIcon.tsx @@ -0,0 +1,24 @@ +import ACHomeLightIcon from "assets/products/Construction/acHomeIconLight.png"; +import ACHomeDarkIcon from "assets/products/Construction/acHomeIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function ACHomeIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? ACHomeLightIcon : ACHomeDarkIcon; + } + + return themeType === "light" ? ACHomeDarkIcon : ACHomeLightIcon; + }; + + return ; +} diff --git a/src/products/Construction/DeviceGroupIcon.tsx b/src/products/Construction/DeviceGroupIcon.tsx new file mode 100644 index 0000000..7129376 --- /dev/null +++ b/src/products/Construction/DeviceGroupIcon.tsx @@ -0,0 +1,29 @@ +import DeviceGroupLightIcon from "assets/products/Construction/deviceGroupIconLight.png"; +import DeviceGroupDarkIcon from "assets/products/Construction/deviceGroupIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + size?: number; + type?: "light" | "dark"; +} + +export default function DeviceGroupIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? DeviceGroupLightIcon : DeviceGroupDarkIcon; + } + + return themeType === "light" ? DeviceGroupDarkIcon : DeviceGroupLightIcon; + }; + + if (size) { + return bins; + } + + return ; +} diff --git a/src/products/Construction/JobSiteIcon.tsx b/src/products/Construction/JobSiteIcon.tsx new file mode 100644 index 0000000..f5f8879 --- /dev/null +++ b/src/products/Construction/JobSiteIcon.tsx @@ -0,0 +1,28 @@ +import JobSiteLightIcon from "assets/products/Construction/jobSiteIconLight.png"; +import JobSiteDarkIcon from "assets/products/Construction/jobSiteIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; + size?: number; +} + +export default function JobsiteIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? JobSiteLightIcon : JobSiteDarkIcon; + } + + return themeType === "light" ? JobSiteDarkIcon : JobSiteLightIcon; + }; + + if (size) { + return jobsite; + } + return ; +} diff --git a/src/products/Construction/NexusSTIcon.tsx b/src/products/Construction/NexusSTIcon.tsx new file mode 100644 index 0000000..9df3db4 --- /dev/null +++ b/src/products/Construction/NexusSTIcon.tsx @@ -0,0 +1,29 @@ +import NexusLightIcon from "assets/products/Construction/nexusIconLight.png"; +import NexusDarkIcon from "assets/products/Construction/nexusIconDark.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + size?: number; + type?: "light" | "dark"; +} + +export default function NexusSTIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? NexusLightIcon : NexusDarkIcon; + } + + return themeType === "light" ? NexusDarkIcon : NexusLightIcon; + }; + + if (size) { + return bins; + } + + return ; +} diff --git a/src/products/Construction/ObjectHeaterIcon.tsx b/src/products/Construction/ObjectHeaterIcon.tsx new file mode 100644 index 0000000..884e0bf --- /dev/null +++ b/src/products/Construction/ObjectHeaterIcon.tsx @@ -0,0 +1,24 @@ +import HeaterDarkIcon from "assets/components/heaterDark.png"; +import HeaterLightIcon from "assets/components/heaterLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function ObjectHeaterIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? HeaterLightIcon : HeaterDarkIcon; + } + + return themeType === "light" ? HeaterDarkIcon : HeaterLightIcon; + }; + + return ; +} diff --git a/src/products/Construction/SitesIcon.tsx b/src/products/Construction/SitesIcon.tsx new file mode 100644 index 0000000..e6b53c3 --- /dev/null +++ b/src/products/Construction/SitesIcon.tsx @@ -0,0 +1,29 @@ +import SitesDarkIcon from "assets/products/Construction/sitesIconDark.png"; +import SitesLightIcon from "assets/products/Construction/sitesIconLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + size?: number; + type?: "light" | "dark"; +} + +export default function SitesIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? SitesLightIcon : SitesDarkIcon; + } + + return themeType === "light" ? SitesDarkIcon : SitesLightIcon; + }; + + if (size) { + return bins; + } + + return ; +} diff --git a/src/products/Construction/TasksIcon.tsx b/src/products/Construction/TasksIcon.tsx new file mode 100644 index 0000000..a416aa9 --- /dev/null +++ b/src/products/Construction/TasksIcon.tsx @@ -0,0 +1,29 @@ +import TasksDarkIcon from "assets/products/Construction/tasksIconDark.png"; +import TasksLightIcon from "assets/products/Construction/tasksIconLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; + size?: number; +} + +export default function TasksIcon(props: Props) { + const themeType = useThemeType(); + const { type, size } = props; + + const src = () => { + if (type) { + return type === "light" ? TasksLightIcon : TasksDarkIcon; + } + + return themeType === "light" ? TasksDarkIcon : TasksLightIcon; + }; + + return size ? ( + tasks + ) : ( + + ); +} diff --git a/src/products/DeviceProduct.ts b/src/products/DeviceProduct.ts new file mode 100644 index 0000000..e48d986 --- /dev/null +++ b/src/products/DeviceProduct.ts @@ -0,0 +1,140 @@ +import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability"; +import { pond, quack } from "protobuf-ts/pond"; +import { + BindaptMini, + BindaptPlus, + BindaptPlusLite, + BindaptPlusMod, + BindaptPlusPro, + BindaptPlusProMod, + BindaptV2Monitor, + BindaptV2Automate, + BinHalo, + BinMonitor, + BinUltimate +} from "./Bindapt/BindaptDescriber"; +import { NexusST } from "./Nexus/NexusDescriber"; +import { MiPCAV2, OmniAir } from "./OmniAir/OmniAirDescriber"; +import { ConfigurablePin } from "pbHelpers/AddressTypes"; + +export type ProductTab = "components" | "presets"; + +export interface DeviceProductDescriber { + product: pond.DeviceProduct; + label: string; + icon: (platform: pond.DevicePlatform, theme?: "light" | "dark") => string | undefined; + tabs: ProductTab[]; + view?: "default" | "controller"; + availability?: DeviceAvailabilityMap; + componentExtension?: Boolean; + defaultAddressMap?: Map; +} + +const None: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_NONE, + label: "None", + icon: () => undefined, + tabs: ["components"] +}; + +//think about making weather describer +const weatherStationPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 32, label: "4" } +]; + +const Weather: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_WEATHER_STATION, + label: "Weather Station", + icon: () => undefined, + tabs: ["components"], + availability: new Map([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, weatherStationPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] + ]) +}; + +const DEVICE_PRODUCT_MAP = new Map([ + [pond.DeviceProduct.DEVICE_PRODUCT_NONE, None], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS, BindaptPlus], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO, BindaptPlusPro], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_LITE, BindaptPlusLite], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_MINI, BindaptMini], + [pond.DeviceProduct.DEVICE_PRODUCT_WEATHER_STATION, Weather], + [pond.DeviceProduct.DEVICE_PRODUCT_NEXUS_ST, NexusST], + [pond.DeviceProduct.DEVICE_PRODUCT_OMNIAIR, OmniAir], + [pond.DeviceProduct.DEVICE_PRODUCT_BIN_HALO, BinHalo], + [pond.DeviceProduct.DEVICE_PRODUCT_BIN_ULTIMATE, BinUltimate], + [pond.DeviceProduct.DEVICE_PRODUCT_BIN_MONITOR, BinMonitor], + [pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2, MiPCAV2], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_MOD, BindaptPlusMod], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_PLUS_PRO_MOD, BindaptPlusProMod], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_MONITOR, BindaptV2Monitor], + [pond.DeviceProduct.DEVICE_PRODUCT_BINDAPT_V2_AUTOMATE, BindaptV2Automate] +]); + +export function ListDeviceProductDescribers(): DeviceProductDescriber[] { + return [...DEVICE_PRODUCT_MAP.values()]; +} + +function getDescriber(product: pond.DeviceProduct): DeviceProductDescriber { + let describer = DEVICE_PRODUCT_MAP.get(product); + return describer ? describer : None; +} + +export function GetDeviceProductLabel(product: pond.DeviceProduct): string { + return getDescriber(product).label; +} + +export function GetDeviceProductIcon( + product: pond.DeviceProduct, + platform: pond.DevicePlatform, + theme?: "dark" | "light" +): string | undefined { + return getDescriber(product).icon(platform, theme); +} + +export function IsCardController(product?: pond.DeviceProduct): boolean { + return product ? getDescriber(product).view === "controller" : false; +} + +export function GetDeviceProductViews(product?: pond.DeviceProduct): boolean { + return product ? getDescriber(product).view === "controller" : false; +} + +export function GetDeviceProductTabs(product?: pond.DeviceProduct): ProductTab[] { + return product ? getDescriber(product).tabs : ["components"]; +} + +export function GetProductAvailability( + product?: pond.DeviceProduct +): DeviceAvailabilityMap | undefined { + return product ? getDescriber(product).availability : undefined; +} + +export function IsExtended(product: pond.DeviceProduct): boolean { + let isExtended = getDescriber(product).componentExtension; + if (isExtended) { + return true; + } else { + return false; + } +} + +export function GetProductDefaults(product: pond.DeviceProduct): Map | undefined { + return getDescriber(product).defaultAddressMap; +} diff --git a/src/products/Nexus/NexusAvailability.ts b/src/products/Nexus/NexusAvailability.ts new file mode 100644 index 0000000..9f9fc57 --- /dev/null +++ b/src/products/Nexus/NexusAvailability.ts @@ -0,0 +1,31 @@ +import { ConfigurablePin } from "pbHelpers/AddressTypes"; +import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability"; +import { quack } from "protobuf-ts/pond"; + +const NexusSTPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 512, label: "C1" } +]; + +export const NexusSTAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, NexusSTPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); diff --git a/src/products/Nexus/NexusDescriber.ts b/src/products/Nexus/NexusDescriber.ts new file mode 100644 index 0000000..99322df --- /dev/null +++ b/src/products/Nexus/NexusDescriber.ts @@ -0,0 +1,17 @@ +import { DeviceProductDescriber } from "products/DeviceProduct"; +import { pond } from "protobuf-ts/pond"; +import { NexusSTAvailability } from "./NexusAvailability"; +import NexusLightIcon from "assets/products/Construction/nexusIconLight.png"; +import NexusDarkIcon from "assets/products/Construction/nexusIconDark.png"; + +export const NexusST: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_NEXUS_ST, + label: "Nexus ST", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? NexusDarkIcon : NexusLightIcon; + }, + view: "controller", + tabs: ["components"], + availability: NexusSTAvailability, + componentExtension: true +}; diff --git a/src/products/OmniAir/OmniAirAvailability.ts b/src/products/OmniAir/OmniAirAvailability.ts new file mode 100644 index 0000000..a9f8f27 --- /dev/null +++ b/src/products/OmniAir/OmniAirAvailability.ts @@ -0,0 +1,59 @@ +import { ConfigurablePin } from "pbHelpers/AddressTypes"; +import { DeviceAvailabilityMap, DevicePositions } from "pbHelpers/DeviceAvailability"; +import { quack } from "protobuf-ts/pond"; + +const OmniAirPins: ConfigurablePin[] = [ + { address: 4, label: "1" }, + { address: 8, label: "2" }, + { address: 16, label: "3" }, + { address: 512, label: "C1" }, + { address: 1024, label: "C2" } +]; + +export const OmniAirAvailability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, OmniAirPins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); + +/*---V2 Stuff starts here---*/ + +const MiPCAV2Pins: ConfigurablePin[] = [ + { address: 1, label: "1" }, + { address: 2, label: "2" }, + { address: 4, label: "3" }, + { address: 256, label: "L1" }, + { address: 512, label: "L2" } +]; + +export const MiPCAV2Availability: DeviceAvailabilityMap = new Map< + quack.AddressType, + DevicePositions +>([ + [quack.AddressType.ADDRESS_TYPE_INVALID, []], + [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, MiPCAV2Pins], + [ + quack.AddressType.ADDRESS_TYPE_I2C, + new Map([ + [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18]], + [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40]], + [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]] + ]) + ], + [quack.AddressType.ADDRESS_TYPE_POWER, [0]], + [quack.AddressType.ADDRESS_TYPE_GPS, [0]], + [quack.AddressType.ADDRESS_TYPE_MODEM, [0]] +]); diff --git a/src/products/OmniAir/OmniAirDescriber.ts b/src/products/OmniAir/OmniAirDescriber.ts new file mode 100644 index 0000000..52f11de --- /dev/null +++ b/src/products/OmniAir/OmniAirDescriber.ts @@ -0,0 +1,31 @@ +import { DeviceProductDescriber } from "products/DeviceProduct"; +import { pond } from "protobuf-ts/pond"; +import { MiPCAV2Availability, OmniAirAvailability } from "./OmniAirAvailability"; +import OmniAirLightIcon from "assets/products/Aviation/OmniAirDeviceIconWhite.png"; +import OmniAirDarkIcon from "assets/products/Aviation/OmniAirDeviceIconBlack.png"; + +export const OmniAir: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_OMNIAIR, + label: "MiPCA V1", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? OmniAirDarkIcon : OmniAirLightIcon; + }, + view: "controller", + tabs: ["components"], + availability: OmniAirAvailability, + componentExtension: true +}; + +/*---V2 Stuff starts here---*/ + +export const MiPCAV2: DeviceProductDescriber = { + product: pond.DeviceProduct.DEVICE_PRODUCT_MIPCA_V2, + label: "MiPCA V2", + icon: (_, theme?: "light" | "dark") => { + return theme === "light" ? OmniAirDarkIcon : OmniAirLightIcon; + }, + view: "controller", + tabs: ["components"], + availability: MiPCAV2Availability, + componentExtension: true +}; diff --git a/src/products/ProductTabChip.tsx b/src/products/ProductTabChip.tsx new file mode 100644 index 0000000..29d9099 --- /dev/null +++ b/src/products/ProductTabChip.tsx @@ -0,0 +1,86 @@ +import { Avatar, ButtonBase, createStyles, makeStyles, Theme, Tooltip } from "@material-ui/core"; +import { Tune as PresetsIcon, ViewList as ComponentsIcon } from "@material-ui/icons"; +import classnames from "classnames"; +import { capitalize } from "lodash"; +import React from "react"; +import { ProductTab } from "./DeviceProduct"; + +const useStyles = makeStyles((theme: Theme) => { + const themeType = theme.palette.type; + const activeBG = theme.palette.secondary.main; + return createStyles({ + avatar: { + color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white, + backgroundColor: "transparent", + width: theme.spacing(5), + height: theme.spacing(5), + border: "1px solid", + borderColor: theme.palette.divider + }, + active: { + color: theme.palette.getContrastText(activeBG), + backgroundColor: activeBG, + border: 0 + }, + customIcon: { + padding: theme.spacing(0.5) + } + }); +}); + +interface Props { + tab: ProductTab; + onClick: (selected: ProductTab) => void; + active?: boolean; +} + +export default function ProductTabChip(props: Props) { + const classes = useStyles(); + const { tab, active, onClick } = props; + + const name = () => { + return capitalize(tab); + }; + + const hasCustomIcon = () => { + switch (tab) { + default: + return false; + } + }; + + const customIconSrc = () => { + switch (tab) { + default: + return undefined; + } + }; + + const icon = () => { + switch (tab) { + case "presets": + return ; + case "components": + return ; + default: + return ; + } + }; + + return ( + + onClick(tab)}> + {!hasCustomIcon() && icon()} + + + ); +} diff --git a/src/products/ventilation/MiningIcon.tsx b/src/products/ventilation/MiningIcon.tsx new file mode 100644 index 0000000..a0438f7 --- /dev/null +++ b/src/products/ventilation/MiningIcon.tsx @@ -0,0 +1,24 @@ +import MineDarkIcon from "assets/components/mineIconDark.png"; +import MineLightIcon from "assets/components/mineIconLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function VentilationIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? MineLightIcon : MineDarkIcon; + } + + return themeType === "light" ? MineDarkIcon : MineLightIcon; + }; + + return ; +} diff --git a/src/products/ventilation/VentilationIcon.tsx b/src/products/ventilation/VentilationIcon.tsx new file mode 100644 index 0000000..158b324 --- /dev/null +++ b/src/products/ventilation/VentilationIcon.tsx @@ -0,0 +1,24 @@ +import VentilationDarkIcon from "assets/products/ventilation/ventIconDark.png"; +import VentilationLightIcon from "assets/products/ventilation/ventIconLight.png"; +import { ImgIcon } from "common/ImgIcon"; +import { useThemeType } from "hooks"; +import React from "react"; + +interface Props { + type?: "light" | "dark"; +} + +export default function VentilationIcon(props: Props) { + const themeType = useThemeType(); + const { type } = props; + + const src = () => { + if (type) { + return type === "light" ? VentilationLightIcon : VentilationDarkIcon; + } + + return themeType === "light" ? VentilationDarkIcon : VentilationLightIcon; + }; + + return ; +} diff --git a/src/providers/Snackbar.tsx b/src/providers/Snackbar.tsx new file mode 100644 index 0000000..56e539d --- /dev/null +++ b/src/providers/Snackbar.tsx @@ -0,0 +1,141 @@ +import { Button, Theme } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { + ProviderContext as NotistackProviderContext, + SnackbarAction, + SnackbarProvider as NotistackSnackbarProvider, + useSnackbar as useNotistackSnackbar +} from "notistack"; +import React, { createContext, PropsWithChildren, useContext } from "react"; + +const useStyles = makeStyles((theme: Theme) => ({ + bottomLeft: { + position: "absolute", + bottom: theme.spacing(7), + left: theme.spacing(1), + [theme.breakpoints.up("sm")]: { + bottom: theme.spacing(1) + } + } +})); + +interface ISnackbarContext { + success: (message: string, action?: SnackbarAction) => void; + warning: (message: string, action?: SnackbarAction) => void; + error: (message: string, action?: SnackbarAction) => void; + info: (message: string, action?: SnackbarAction) => void; + openSnack: (message: string, action?: SnackbarAction) => void; + closeSnackbar: (key?: string | number | undefined) => void; +} + +type Status = "error" | "success" | "warning" | "info" | "default"; +let openExternalSnack: (status: Status, message: string) => void; +export const SnackbarContext = createContext({} as ISnackbarContext); + +interface Props {} + +function SnackbarHelper(props: PropsWithChildren) { + const { children } = props; + const { enqueueSnackbar, closeSnackbar } = useNotistackSnackbar(); + + const openNewSnack = (status: Status, message: string) => { + switch (status) { + case "success": + success(message); + break; + case "warning": + warning(message); + break; + case "error": + error(message); + break; + case "info": + info(message); + break; + default: + openSnack(message); + break; + } + }; + openExternalSnack = openNewSnack; + + const openSnack = (message: string, action?: SnackbarAction) => { + enqueueSnackbar(message, { variant: "default", action }); + }; + + const success = (message: string, action?: SnackbarAction) => { + enqueueSnackbar(message, { variant: "success", action }); + }; + + const warning = (message: string, action?: SnackbarAction) => { + enqueueSnackbar(message, { variant: "warning", action }); + }; + + const error = (message: string, action?: SnackbarAction) => { + let msg = message as any; + if (typeof message !== "string") { + if (Object.keys(message).includes("response")) { + msg = msg.response.data.error; + } + } + enqueueSnackbar(msg, { variant: "error", action }); + }; + + const info = (message: string, action?: SnackbarAction) => { + enqueueSnackbar(message, { variant: "info", action }); + }; + + return ( + + {children} + + ); +} + +export default function SnackbarProvider(props: PropsWithChildren) { + const { children } = props; + const classes = useStyles(); + const notistackRef = React.createRef(); + + const onClickDismiss = (key: React.ReactText) => () => { + if (notistackRef && notistackRef.current) notistackRef.current.closeSnackbar(key); + }; + + return ( + ( + + )} + classes={{ + containerAnchorOriginBottomLeft: classes.bottomLeft + }}> + {children} + + ); +} + +export const useSnackbar = () => useContext(SnackbarContext); + +// Snackbar access for external sources that are not React components +export const openSnackbar = (status: Status, message: string) => { + if (openExternalSnack === undefined) { + console.warn("openSnackbar is undefined - most likely not loaded into state yet"); + } else { + openExternalSnack(status, message); + } +}; diff --git a/src/providers/index.ts b/src/providers/index.ts index d484f40..3296f88 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -4,7 +4,7 @@ export { HTTPContext, useHTTP } from "./http"; export { // useBackpackAPI, - // useBinAPI, + useBinAPI, // useBinYardAPI, // useNoteAPI, // useComponentAPI, @@ -22,11 +22,12 @@ export { // useInteractionsAPI, // useMeasurementsWebsocket, // useMetricAPI, - // usePermissionAPI, + usePermissionAPI, // usePreferenceAPI, // useSiteAPI, // useTagAPI, - // useTeamAPI, + useTeamAPI, + useImagekitAPI, // useTaskAPI, // useUsageAPI, useUserAPI, @@ -35,7 +36,7 @@ export { // useMineAPI, // useKeyManagerAPI, // useTerminalAPI, - // useGateAPI, + useGateAPI, // useObjectHeaterAPI, // useMutationAPI, // useGrainBagAPI, @@ -43,7 +44,7 @@ export { // useFileControllerAPI } from "./pond/pond"; // export { SecurityContext, useSecurity } from "./security"; -// export { SnackbarContext, useSnackbar } from "./Snackbar"; +export { SnackbarContext, useSnackbar } from "./Snackbar"; export * from "./StateContainer"; // export { TagsContext, TagsProvider, useTags } from "./tags"; -// export { UsersContext, UsersProvider, useUsers } from "./users"; +// export { UsersContext, UsersProvider, useUsers } from "./users"; \ No newline at end of file diff --git a/src/providers/pond/binAPI.tsx b/src/providers/pond/binAPI.tsx new file mode 100644 index 0000000..c95941b --- /dev/null +++ b/src/providers/pond/binAPI.tsx @@ -0,0 +1,499 @@ +import { useHTTP, usePermissionAPI } from "hooks"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; +import { User, binScope } from "models"; +import { pondURL } from "./pond"; +import { AxiosResponse } from "axios"; +import { dateRange } from "providers/http"; +import { useGlobalState } from "providers"; +import { quack } from "protobuf-ts/quack"; + +export interface IBinAPIContext { + addBin: (bin: pond.BinSettings) => Promise>; + updateBin: (key: string, bin: pond.BinSettings) => Promise>; + bulkBinUpdate: (bins: pond.BinSettings[]) => Promise>; + updateBinStatus: ( + key: string, + binStatus: pond.BinStatus + ) => Promise>; + removeBin: (key: string) => Promise>; + getBin: (key: string) => Promise>; + addComponent: ( + bin: string, + device: number, + component: string, + preferences?: pond.BinComponentPreferences + ) => Promise>; + removeComponent: ( + bin: string, + component: string + ) => Promise>; + removeAllComponents: (bin: string) => Promise>; + updateComponentPreferences: ( + bin: string, + component: string, + preferences: pond.BinComponentPreferences + ) => Promise>; + getBinPageData: (binKey: string, userKey: string, showErrors?: boolean) => Promise; + listBins: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean, + numerical?: boolean, + keys?: string[], + types?: string[] + ) => Promise>; + listBinsAndData: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean, + keys?: string[], + types?: string[] + ) => Promise>; + listHistory: ( + id: string, + limit: number, + offset: number + ) => Promise>; + listHistoryBetween: ( + id: string, + limit: number, + start: string, + end: string + ) => Promise>; + listBinStatus: ( + key: string, + startDate: any, + endDate: any, + limit: number, + offset: number, + order?: "asc" | "desc", + asRoot?: boolean + ) => Promise>; + listBinComponents: (key: string) => Promise>; + listBinComponentsMeasurements: ( + key: string, + start: string, + end: string, + showErrors?: boolean + ) => Promise>; + updateBinPermissions: ( + key: string, + users: User[] + ) => Promise>; + getBinMetrics: (search?: string) => Promise>; + listBinLiters: ( + key: string, + start: string, + end: string, + moisture: number, + plenum: string, + cable: string, + pressure: string, + fan?: string // the fan controller is optional so that if they don't have one the fan is assumed on + ) => Promise>; + listBinPrefs: (key: string) => Promise>; + updateTopNodes: ( + key: string, + newTopNodes: pond.NewTopNode[] + ) => Promise>; + listBinMeasurements: ( + key: string, + startDate: any, + endDate: any, + limit: number, + offset: number, + order: string, + measurementType?: number | quack.MeasurementType, + keys?: string[], + types?: string[], + showErrors?: boolean + ) => Promise>; +} + +export const BinAPIContext = createContext({} as IBinAPIContext); + +interface Props {} + +export default function DeviceProvider(props: PropsWithChildren) { + const { children } = props; + const { get, post, put, del } = useHTTP(); + const permissionAPI = usePermissionAPI(); + const [{ as }] = useGlobalState(); + + const addBin = (bin: pond.BinSettings) => { + if (as) return post(pondURL("/bins?as=" + as), bin); + return post(pondURL("/bins"), bin); + }; + + const updateBin = (key: string, bin: pond.BinSettings) => { + if (as) return put(pondURL("/bins/" + key + "?as=" + as), bin); + return put(pondURL("/bins/" + key), bin); + }; + + const bulkBinUpdate = (bins: pond.BinSettings[]) => { + if (as) + return put(pondURL("/bulkBins/update?as=" + as), { bins: bins }); + return put(pondURL("/bulkBins/update"), { bins: bins }); + }; + + const updateBinStatus = (key: string, binStatus: pond.BinStatus) => { + if (as) + return put( + pondURL("/bins/" + key + "/status?as=" + as), + binStatus + ); + return put(pondURL("/bins/" + key + "/status"), binStatus); + }; + + const removeBin = (key: string) => { + if (as) return del(pondURL("/bins/" + key + "?as=" + as)); + return del(pondURL("/bins/" + key)); + }; + + const getBin = (key: string) => { + if (as) return get(pondURL("/bins/" + key + "?as=" + as)); + return get(pondURL("/bins/" + key)); + }; + + const addComponent = ( + bin: string, + device: number, + component: string, + preferences?: pond.BinComponentPreferences + ) => { + let url = "/bins/" + bin + "/addComponent/" + device + "/" + component; + if (as) { + url = + url + + "?as=" + + as + + (preferences ? "&binPref=" + preferences.type + "&fillNode=" + preferences.node : ""); + } else if (preferences) { + url = url + "?binPref=" + preferences.type + "&fillNode=" + preferences.node; + } + return post(pondURL(url)); + }; + + const removeComponent = (bin: string, component: string) => { + let url = "/bins/" + bin + "/removeComponent/" + component; + if (as) url = url + "?as=" + as; + return post(pondURL(url)); + }; + + const removeAllComponents = (bin: string) => { + let url = "/bins/" + bin + "/removeAllComponents"; + if (as) url = url + "?as=" + as; + return post(pondURL(url)); + }; + + const updateComponentPreferences = ( + bin: string, + component: string, + preferences: pond.BinComponentPreferences + ) => { + let url = "/bins/" + bin + "/updateComponent/" + component + "/preferences"; + if (as) url = url + "/?as=" + as; + interface request { + preferences: Object; + } + let body: request = { preferences: pond.BinComponentPreferences.toObject(preferences) }; + return put(pondURL(url), body); + }; + + const getBinPageData = (binKey: string, userKey: string, showErrors = true) => { + return get( + pondURL( + "/bins/" + + binKey + + "/users/" + + userKey + + "?as=" + + as + + (showErrors ? "&showErrors=true" : "&showErrors=false") + ) + ); + }; + + const listBins = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean, + numerical?: boolean, + keys?: string[], + types?: string[] + ) => { + return get( + pondURL( + "/bins" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&by=" + or(orderBy, "key")) + + (numerical ? "&numerical=true" : "") + + (as ? "&as=" + as : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (search ? "&search=" + search : "") + + (keys ? "&keys=" + keys.join(",") : "") + + (types ? "&types=" + types.join(",") : "") + ) + ); + }; + + const listBinsAndData = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean, + keys?: string[], + types?: string[] + ) => { + return get( + pondURL( + "/binsAndData" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&by=" + or(orderBy, "key")) + + (as ? "&as=" + as : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (search ? "&search=" + search : "") + + (keys ? "&keys=" + keys.join(",") : "") + + (types ? "&types=" + types.join(",") : "") + ) + ); + }; + + const listBinStatus = ( + key: string, + startDate: any, + endDate: any, + limit: number, + offset: number, + order?: "asc" | "desc", + asRoot?: boolean + ) => { + return get( + pondURL( + "/bins/" + + key + + "/status" + + dateRange(startDate, endDate) + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + ) + ); + }; + + const listBinComponents = (key: string) => { + return get(pondURL("/bins/" + key + "/components")); + }; + + const listBinComponentsMeasurements = ( + key: string, + start: string, + end: string, + showErrors = false + ) => { + return get( + pondURL( + "/bins/" + + key + + "/components/measurements?start=" + + start + + "&end=" + + end + + "&as=" + + as + + (showErrors ? "&showErrors=true" : "&showErrors=false") + ) + ); + }; + + const listHistory = (id: string, limit: number, offset: number) => { + return get( + pondURL("/bins/" + id + "/history?limit=" + limit + "&offset=" + offset) + ); + }; + + const listHistoryBetween = (id: string, limit: number, start: string, end: string) => { + return get( + pondURL("/bins/" + id + "/historyBetween?limit=" + limit + "&start=" + start + "&end=" + end) + ); + }; + + const updateBinPermissions = (key: string, users: User[]) => { + return permissionAPI.updatePermissions(binScope(key.toString()), users); + }; + + const getBinMetrics = (search?: string) => { + if (as) { + if (search) + return get( + pondURL("/metrics/bins/?search=" + search + "&as=" + as) + ); + return get(pondURL("/metrics/bins/?as=" + as)); + } + return get( + pondURL("/metrics/bins" + (search ? "?search=" + search : "")) + ); + }; + + const listBinLiters = ( + key: string, + start: string, + end: string, + moisture: number, + plenum: string, + cable: string, + pressure: string, + fan?: string + ) => { + if (as) { + return get( + pondURL( + "/bins/" + + key + + "/liters?start=" + + start + + "&end=" + + end + + "&moisture=" + + moisture + + "&plenum=" + + plenum + + "&cable=" + + cable + + "&pressure=" + + pressure + + (fan ? "&fan=" + fan : "") + + "&as=" + + as + ) + ); + } + return get( + pondURL( + "/bins/" + + key + + "/liters?start=" + + start + + "&end=" + + end + + "&moisture=" + + moisture + + "&plenum=" + + plenum + + "&cable=" + + cable + + "&pressure=" + + pressure + + (fan ? "&fan=" + fan : "") + ) + ); + }; + + const listBinPrefs = (key: string) => { + return get( + pondURL("/bins/" + key + "/componentPreferences?as=" + as) + ); + }; + + const updateTopNodes = (key: string, newTopNodes: pond.NewTopNode[]) => { + let body: pond.TopNodeList = pond.TopNodeList.create({ newTopNodes: newTopNodes }); + return post( + pondURL("/bins/" + key + "/updateTopNodes" + (as ? "?as=" + as : "")), + body + ); + }; + + const listBinMeasurements = ( + key: string, + startDate: any, + endDate: any, + limit: number, + offset: number, + order: string, + measurementType?: number | quack.MeasurementType, + keys?: string[], + types?: string[], + showErrors?: boolean + ) => { + const url = pondURL( + "/objects/" + + key + + "/1/unitMeasurements" + + dateRange(startDate, endDate) + + "&limit=" + + limit + + "&offset=" + + offset + + "&order=" + + order + + (measurementType ? "&type=" + measurementType : "") + + (as ? "&as=" + as : "") + + (keys ? "&keys=" + keys : "") + + (types ? "&types=" + types : "") + + (showErrors ? "&showErrors=true" : "&showErrors=false") + ); + return get(url); + }; + + return ( + + {children} + + ); +} + +export const useBinAPI = () => useContext(BinAPIContext); diff --git a/src/providers/pond/gateAPI.tsx b/src/providers/pond/gateAPI.tsx new file mode 100644 index 0000000..6e82812 --- /dev/null +++ b/src/providers/pond/gateAPI.tsx @@ -0,0 +1,267 @@ +import { useHTTP } from "hooks"; +import React, { createContext, PropsWithChildren, useContext } from "react"; +import { pond } from "protobuf-ts/pond"; +import { pondURL } from "./pond"; +import { AxiosResponse } from "axios"; +import { useGlobalState } from "providers"; + +export interface IGateInterface { + addGate: ( + name: string, + settings: pond.GateSettings + ) => Promise>; + listGates: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + pcaStatus?: boolean, + keys?: string[], + types?: string[], + numerical?: boolean, + specificUser?: string + ) => Promise>; + updateGate: ( + key: string, + name: string, + settings: pond.GateSettings + ) => Promise>; + getGate: (key: string) => Promise>; + removeGate: (key: string) => Promise>; + updateLink: ( + parentKey: string, + parentType: string, + objectID: string, + objectType: string, + permissions: string[] + ) => Promise; + updatePrefs: ( + gateKey: string, + childType: string, + childKey: string, + gatePref: pond.GateComponentType | pond.GateDeviceType, + keys?: string[], + types?: string[] + ) => Promise>; + getGatePageData: (key: string) => Promise>; + listGateAirflow: ( + gate: string, + device: number | string, + ambientComponent: string, + pressureComponent: string, + start: string, + end: string, + keys?: string[], + types?: string[] + ) => Promise>; + listGateMeasurements: ( + key: string, + start: string, + end: string + ) => Promise>; +} + +export const GateAPIcontext = createContext({} as IGateInterface); + +interface Props {} + +export default function GateProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, post, put } = useHTTP(); + const [{ as }] = useGlobalState(); + + const addGate = (name: string, settings: pond.GateSettings) => { + if (as) { + return post(pondURL("/gates?name=" + name + "&as=" + as), settings); + } + return post(pondURL("/gates?name=" + name + "&as=" + as), settings); + }; + + const listGates = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + pcaStatus?: boolean, + keys?: string[], + types?: string[], + numerical?: boolean, + specificUser?: string + ) => { + let asText = ""; + if (as) asText = "&as=" + as; + if (specificUser) asText = "&as=" + specificUser; + return get( + pondURL( + "/gates?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + (order ? order : "asc")) + + ("&by=" + (orderBy ? orderBy : "key")) + + (numerical ? "&numerical=true" : "") + + (search ? "&search=" + search : "") + + (pcaStatus ? "&getPCA=" + pcaStatus.toString() : "") + + (keys ? "&keys=" + keys.toString() : "") + + (types ? "&types=" + types.toString() : "") + + asText + ) + ); + }; + + const updateGate = (key: string, name: string, settings: pond.GateSettings) => { + if (as) { + return put( + pondURL("/gates/" + key + "?as=" + as + "&name=" + name), + settings + ); + } + return put(pondURL("/gates/" + key + "?name=" + name), settings); + }; + + const removeGate = (key: string) => { + if (as) { + return del(pondURL("/gates/" + key + "?as=" + as)); + } + return del(pondURL("/gates/" + key + "?as=" + as)); + }; + + const getGate = (key: string) => { + if (as) { + return get(pondURL("/gates/" + key + "?as=" + as)); + } + return get(pondURL("/gates/" + key)); + }; + + const getGatePageData = (key: string) => { + if (as) { + return get(pondURL("/gatePage/" + key + "?as=" + as)); + } + return get(pondURL("/gatePage/" + key)); + }; + + const updateLink = ( + parentID: string, + parentType: string, + objectID: string, + objectType: string, + permissions: string[] + ) => { + if (as) + return post(pondURL(`/gates/` + parentID + `/link?as=${as}`), { + Key: objectID, + Type: objectType, + Parent: parentID, + ParentType: parentType, + Permissions: permissions + }); + return post(pondURL(`/gates/` + parentID + `/link`), { + Key: objectID, + Type: objectType, + Parent: parentID, + ParentType: parentType, + Permissions: permissions + }); + }; + + const updatePrefs = ( + gateKey: string, + childType: string, + childKey: string, + gatePref: pond.GateComponentType | pond.GateDeviceType, + keys?: string[], + types?: string[] + ) => { + if (as) { + return put( + pondURL( + `/gatePrefs/` + + gateKey + + "?childType=" + + childType + + "&childKey=" + + childKey + + "&gatePreference=" + + gatePref + + `&as=` + + as + + (keys ? "&keys=" + keys.toString() : "") + + (types ? "&types=" + types.toString() : "") + ) + ); + } + return put( + pondURL( + `/gatePrefs/` + + gateKey + + "?childType=" + + childType + + "&childKey=" + + childKey + + "&gatePreference=" + + gatePref + + (keys ? "&keys=" + keys.toString() : "") + + (types ? "&types=" + types.toString() : "") + ) + ); + }; + + const listGateAirflow = ( + gate: string, + device: number | string, + ambientComponent: string, + pressureComponent: string, + start: string, + end: string, + keys?: string[], + types?: string[] + ) => { + return get( + pondURL( + "/gates/" + + gate + + "/airflow?device=" + + device + + "&ambient=" + + ambientComponent + + "&pressure=" + + pressureComponent + + "&start=" + + start + + "&end=" + + end + + (as ? "&as=" + as : "") + + (keys ? "&keys=" + keys.toString() : "") + + (types ? "&types=" + types.toString() : "") + ) + ); + }; + + const listGateMeasurements = (key: string, start: string, end: string) => { + return get( + pondURL("/gates/" + key + "/measurements?start=" + start + "&end=" + end + "&as=" + as) + ); + }; + + return ( + + {children} + + ); +} + +export const useGateAPI = () => useContext(GateAPIcontext); diff --git a/src/providers/pond/imagekitAPI.tsx b/src/providers/pond/imagekitAPI.tsx new file mode 100644 index 0000000..d387241 --- /dev/null +++ b/src/providers/pond/imagekitAPI.tsx @@ -0,0 +1,45 @@ +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pondURL } from "./pond"; +import { AxiosResponse } from "axios"; + +export interface IImagekitAPIContext { + uploadImage: ( + filename: string, + file: any, + filepath?: string + ) => Promise>; +} + +export const ImagekitAPIContext = createContext({} as IImagekitAPIContext); + +interface Props {} + +export default function ImagekitProvider(props: PropsWithChildren) { + const { children } = props; + const { post } = useHTTP(); + const [{ as }] = useGlobalState(); + + const uploadImage = (filename: string, file: any, filepath = "/") => { + let request = new pond.UploadImageRequest(); + request.file = file; + request.fileName = filename; + request.filePath = filepath; + + if (as) return post(pondURL("/images/upload?as=" + as), request); + return post(pondURL("/images/upload"), request); + }; + + return ( + + {children} + + ); +} + +export const useImagekitAPI = () => useContext(ImagekitAPIContext); diff --git a/src/providers/pond/permissionAPI.tsx b/src/providers/pond/permissionAPI.tsx new file mode 100644 index 0000000..06e4ac1 --- /dev/null +++ b/src/providers/pond/permissionAPI.tsx @@ -0,0 +1,153 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { Scope, Team, User } from "models"; +import { permissionToString } from "pbHelpers/Permission"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { objectQueryParams, pondURL } from "./pond"; + +export interface IPermissionAPIContext { + getPermissions: (user: string, scope: Scope) => Promise>; + removePermissions: (user: string, scope: Scope) => Promise>; + updatePermissions: (scope: Scope, users: User[] | Team[]) => Promise>; + updateRelativePermissions: ( + parentScope: Scope, + childScope: Scope, + permissions: pond.Permission[] + ) => Promise>; + shareObject: ( + scope: Scope, + email: string, + permissions: pond.Permission[], + useImitation?: boolean + ) => Promise>; + shareObjectByKey: ( + scope: Scope, + key: string, + permissions: pond.Permission[] + ) => Promise>; + addShareableLink: (scope: Scope, expiration: string) => Promise>; + removeShareableLink: (scope: Scope, code: string) => Promise>; + listShareableLinks: (scope: Scope) => Promise>; + getShareableLink: (scope: Scope, code: string) => Promise>; +} + +export const PermissionAPIContext = createContext( + {} as IPermissionAPIContext +); + +interface Props {} + +export default function PermissionProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, put, post } = useHTTP(); + const [{ as }] = useGlobalState(); + + const getPermissions = (user: string, scope: Scope) => { + return get(pondURL("/users/" + user + "/permissions" + objectQueryParams(scope))); + }; + + const removePermissions = (user: string, scope: Scope) => { + return del(pondURL("/users/" + user + "/permissions" + objectQueryParams(scope))); + }; + + const updatePermissions = (scope: Scope, users: User[] | Team[]) => { + interface user { + id: string; + permissions: string[]; + } + interface request { + users: user[]; + } + let body: request = { users: [] }; + users.forEach((user: User | Team) => { + body.users.push({ + id: user.id(), + permissions: user.permissions.map(permission => permissionToString(permission)) + }); + }); + return put(pondURL("/" + scope.kind + "s/" + scope.key + "/permissions"), body); + }; + + const shareObject = ( + scope: Scope, + email: string, + permissions: pond.Permission[], + useImitation = true + ) => { + let sql = "/" + scope.kind + "s/" + scope.key + "/share"; + if (as && useImitation) sql = sql + "?as=" + as; + return post(pondURL(sql), { + email: email, + permissions: permissions.map(permission => permissionToString(permission)) + }); + }; + + const updateRelativePermissions = ( + parentScope: Scope, + childScope: Scope, + permissions: pond.Permission[] + ) => { + let sql = "/updateRelativePermissions"; + if (as) sql = sql + "?as=" + as; + return put(pondURL(sql), { + parent: parentScope.key, + parentType: parentScope.kind, + child: childScope.key, + childType: childScope.kind, + permissions: permissions.map(permission => permissionToString(permission)) + }); + }; + + const shareObjectByKey = (scope: Scope, key: string, permissions: pond.Permission[]) => { + if (as) + return post(pondURL(`/${scope.kind}s/${scope.key}/shareByKey?as=${as}`), { + key: key, + permissions: permissions.map(permission => permissionToString(permission)) + }); + return post(pondURL("/" + scope.kind + "s/" + scope.key + "/shareByKey"), { + key: key, + permissions: permissions.map(permission => permissionToString(permission)) + }); + }; + + const addShareableLink = (scope: Scope, expiration: string) => { + let shareBody: pond.ShareableLinkSettings = pond.ShareableLinkSettings.create({ + expiration: expiration + }); + return post(pondURL("/" + scope.kind + "s/" + scope.key + "/links"), shareBody); + }; + + const removeShareableLink = (scope: Scope, code: string) => { + return del(pondURL("/" + scope.kind + "s/" + scope.key + "/links/" + code)); + }; + + const listShareableLinks = (scope: Scope) => { + return get(pondURL("/" + scope.kind + "s/" + scope.key + "/links")); + }; + + const getShareableLink = (scope: Scope, code: string) => { + return get(pondURL("/" + scope.kind + "s/" + scope.key + "/links/" + code)); + }; + + return ( + + {children} + + ); +} + +export const usePermissionAPI = () => useContext(PermissionAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index e786baf..ef6deb8 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -1,5 +1,11 @@ import { PropsWithChildren } from "react"; import UserProvider, { useUserAPI } from "./userAPI"; +import TeamProvider, { useTeamAPI } from "./teamAPI"; +import { useImagekitAPI } from "./imagekitAPI"; +import { usePermissionAPI } from "./permissionAPI"; +import { Scope } from "models"; +import { useBinAPI } from "./binAPI"; +import { useGateAPI } from "./gateAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { let url = import.meta.env.VITE_APP_API_URL + (demo ? "/demo" : "") + partial; @@ -8,20 +14,27 @@ export const pondURL = (partial: string, demo: boolean = false): string => { return url; }; -// export const objectQueryParams = (scope: Scope) => { -// return "?" + scope.kind + "_id=" + scope.key; -// }; +export const objectQueryParams = (scope: Scope) => { + return "?" + scope.kind + "_id=" + scope.key; +}; export default function PondProvider(props: PropsWithChildren) { const { children } = props; return ( - {children} + + {children} + ); } export { useUserAPI, + useTeamAPI, + useImagekitAPI, + usePermissionAPI, + useBinAPI, + useGateAPI }; diff --git a/src/providers/pond/teamAPI.tsx b/src/providers/pond/teamAPI.tsx index 561b901..e17e31b 100644 --- a/src/providers/pond/teamAPI.tsx +++ b/src/providers/pond/teamAPI.tsx @@ -73,8 +73,8 @@ export default function TeamProvider(props: PropsWithChildren) { const updatePreferences = ( key: string, preferences: pond.TeamPreferences, - keys?: string[], - types?: string[] + // keys?: string[], + // types?: string[] ) => { return put(pondURL("/teams/" + key + "/preferences"), preferences); }; diff --git a/src/providers/pond/userAPI.tsx b/src/providers/pond/userAPI.tsx index 7354e23..419f13d 100644 --- a/src/providers/pond/userAPI.tsx +++ b/src/providers/pond/userAPI.tsx @@ -2,15 +2,15 @@ // import { Scope, User } from "models"; // import { pond } from "protobuf-ts/pond"; import { createContext, PropsWithChildren, useContext } from "react"; -// import { objectQueryParams, pondURL } from "./pond"; +import { objectQueryParams, pondURL } from "./pond"; // import { or } from "utils"; -// import { useGlobalState } from "providers"; +import { useGlobalState } from "providers"; // import { AxiosResponse } from "axios"; import { useHTTP } from "../http"; -import { pondURL } from "./pond"; import { AxiosResponse } from "axios"; import { pond } from "protobuf-ts/pond"; import { User } from "../../models/user"; +import { Scope } from "models"; // export interface ListUsersResponse { // users: User[]; @@ -26,9 +26,9 @@ export interface IUserAPIContext { // search?: string // ) => Promise; // listProfiles: () => Promise; -// listObjectUsers: (scope: Scope) => Promise; -// updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise; - getUser: (id: string) => Promise; + listObjectUsers: (scope: Scope) => Promise; + updateObjectUsers: (scope: Scope, users: pond.IUser[]) => Promise; + getUser: (id: string, scope?: Scope) => Promise; getUserWithTeam: ( id: string, team?: string, @@ -53,8 +53,8 @@ export const UserAPIContext = createContext({} as IUserAPIConte export default function UserProvider(props: PropsWithChildren) { const { children } = props; - const { get, /*put*/ } = useHTTP(); - // const [{ as }] = useGlobalState(); + const { get, put } = useHTTP(); + const [{ as }] = useGlobalState(); // const listUsers = ( // limit: number, @@ -104,20 +104,20 @@ export default function UserProvider(props: PropsWithChildren) { // }); // }; - // const listObjectUsers = (scope: Scope) => { - // let sql = "/" + scope.kind + "s/" + scope.key + "/users"; - // if (as) sql = sql + "?as=" + as; - // return get(pondURL(sql)); - // }; + const listObjectUsers = (scope: Scope) => { + let sql = "/" + scope.kind + "s/" + scope.key + "/users"; + if (as) sql = sql + "?as=" + as; + return get(pondURL(sql)); + }; - // const updateObjectUsers = (scope: Scope, users: pond.IUser[]) => { - // return put(pondURL("/users" + objectQueryParams(scope)), { users }); - // }; + const updateObjectUsers = (scope: Scope, users: pond.IUser[]) => { + return put(pondURL("/users" + objectQueryParams(scope)), { users }); + }; // const getUser = (id: string): Promise => { - const getUser = (id: string): Promise => { + const getUser = (id: string, scope?: Scope): Promise => { let partial = "/users/" + id; - // if (scope) partial += "?kind=" + scope.kind + "&key=" + scope.key; + if (scope) partial += "?kind=" + scope.kind + "&key=" + scope.key; return new Promise(function(resolve, reject) { get(pondURL(partial)) .then((response: any) => resolve(response.data)) @@ -218,8 +218,8 @@ export default function UserProvider(props: PropsWithChildren) { value={{ // listUsers, // listProfiles, - // listObjectUsers, - // updateObjectUsers, + listObjectUsers, + updateObjectUsers, getUser, getUserWithTeam, // getProfile, diff --git a/src/teams/TeamActions.tsx b/src/teams/TeamActions.tsx new file mode 100644 index 0000000..5e49400 --- /dev/null +++ b/src/teams/TeamActions.tsx @@ -0,0 +1,197 @@ +import { + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Tooltip +} from "@mui/material"; +import RemoveSelfIcon from "@mui/icons-material/ExitToApp"; +import MoreIcon from "@mui/icons-material/MoreVert"; +import GroupSettingsIcon from "@mui/icons-material/Settings"; +import ShareObjectIcon from "@mui/icons-material/Share"; +import ObjectUsersIcon from "@mui/icons-material/AccountCircle"; +import { Team, teamScope } from "models"; +import { pond } from "protobuf-ts/pond"; +import React, { useState } from "react"; +import ObjectUsers from "user/ObjectUsers"; +import RemoveSelfFromObject from "user/RemoveSelfFromObject"; +import ShareObject from "user/ShareObject"; +import { isOffline } from "utils/environment"; +import TeamSettings from "./TeamSettings"; +import NotificationButton from "common/NotificationButton"; +import { blue } from "@mui/material/colors"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles(() => ({ + shareIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + removeIcon: { + color: "var(--status-alert)" + }, + red: { + color: "var(--status-alert)" + } +})); + +interface Props { + team: Team; + permissions: pond.Permission[]; + refreshCallback: () => void; + preferences: pond.TeamPreferences; + toggleNotificationPreference: () => void; +} + +interface OpenState { + share: boolean; + users: boolean; + teams: boolean; + settings: boolean; + removeSelf: boolean; +} + +export default function TeamActions(props: Props) { + const classes = useStyles(); + const { team, permissions, refreshCallback, preferences, toggleNotificationPreference } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [openState, setOpenState] = useState({ + share: false, + users: false, + teams: false, + settings: false, + removeSelf: false + }); + + const groupMenu = () => { + const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE); + const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS); + return ( + setAnchorEl(null)} + keepMounted + disableAutoFocusItem> + {!isOffline() && canShare && ( + { + setOpenState({ ...openState, share: true }); + setAnchorEl(null); + }} + // button + > + + + + + + )} + {!isOffline() && canManageUsers && ( + { + setOpenState({ ...openState, users: true }); + setAnchorEl(null); + }} + // button + > + + + + + + )} + { + setOpenState({ ...openState, removeSelf: true }); + setAnchorEl(null); + }} + // button + > + + + + + + + ); + }; + + const dialogs = () => { + const key = team.key(); + const label = team.name(); + return ( + + { + setOpenState({ ...openState, settings: false }); + }} + refreshCallback={refreshCallback} + /> + setOpenState({ ...openState, share: false })} + /> + setOpenState({ ...openState, users: false })} + refreshCallback={refreshCallback} + /> + setOpenState({ ...openState, removeSelf: false })} + /> + + ); + }; + + const canWrite = permissions.includes(pond.Permission.PERMISSION_WRITE); + return ( + + {/* + ); +} diff --git a/src/teams/TeamList.tsx b/src/teams/TeamList.tsx new file mode 100644 index 0000000..43820f9 --- /dev/null +++ b/src/teams/TeamList.tsx @@ -0,0 +1,360 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { + Card, + Theme, + List, + ListItem, + ListItemAvatar, + Avatar, + ListItemText, + Typography, + ListItemSecondaryAction, + Divider, + useTheme, + TextField, + InputAdornment, + CircularProgress, + IconButton, + MenuItem, +} from "@mui/material"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState, useTeamAPI, useUserAPI } from "providers"; +import { Team, teamScope } from "models"; +import { useNavigate } from "react-router"; +// import TeamActions from "./TeamActions"; +import SearchIcon from '@mui/icons-material/Search'; +import NextIcon from "@mui/icons-material/NavigateNext"; +import PrevIcon from "@mui/icons-material/NavigateBefore"; +import LastIcon from "@mui/icons-material/SkipNext"; +import FirstIcon from "@mui/icons-material/SkipPrevious"; +import TeamSettings from "./TeamSettings"; +import { Add } from "@mui/icons-material"; +import { cloneDeep } from "lodash"; +import { getContextKeys, getContextTypes } from "pbHelpers/Context"; +import { makeStyles, styled } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + cardContent: { + padding: theme.spacing(1) + }, + clickable: { + "&:hover": { + textDecoration: "underline", + cursor: "pointer" + } + }, + input: { + marginLeft: "auto", + marginRight: theme.spacing(2), + marginTop: theme.spacing(1) + } +})) + +const limits = [ + { + value: 5, + label: "5" + }, + { + value: 10, + label: "10" + }, + { + value: 15, + label: "15" + }, + { + value: 20, + label: "20" + } +]; + +interface Props {} + +export default function TeamList(props: Props) { + const classes = useStyles(); + const [teams, setTeams] = useState([]); + const teamAPI = useTeamAPI(); + const userAPI = useUserAPI(); + const history = useNavigate(); + const theme = useTheme(); + const [{ user, as }] = useGlobalState(); + const [teamPerms, setTeamPerms] = useState([]); + const [teamPrefs, setTeamPrefs] = useState([]); + const [total, setTotal] = useState(0); + const [limit, setLimit] = useState(5); + const [page, setPage] = useState(0); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(""); + const [teamDialogOpen, setTeamDialogOpen] = useState(false); + const [asUser, setAsUser] = useState(undefined); + const [userId, setUserId] = useState(undefined); + const [isUser, setIsUser] = useState(true); + + // const ColorAdd = styled(Add)((theme: Theme) => ({ + // color: theme.palette.getContrastText(theme.palette.primary.main), + // backgroundColor: theme.palette.primary.main, + // marginTop: 'auto', + // marginBottom: 'auto', + // borderRadius: '4px', + // '&:hover': { + // backgroundColor: theme.palette.primary.light, + // cursor: 'pointer', + // }, + // '&:disabled': { + // backgroundColor: 'rgba(0,0,0,0)', + // color: theme.palette.getContrastText(theme.palette.primary.main), + // }, + // })); + + useEffect(() => { + if (as.includes("auth0")) setAsUser(as); + }, [as]); + + useEffect(() => { + setUserId(user.id()); + }, [user]); + + useEffect(() => { + if (!asUser) { + setIsUser(true); + return; + } + setIsUser(userId === asUser); + }, [userId, asUser]); + + const loadTeams = useCallback(() => { + setLoading(true); + let newTeamPerms: pond.Permission[][] = []; + let newTeamPrefs: pond.TeamPreferences[] = []; + let searchString = search; + teamAPI + .listTeams(limit, limit * page, undefined, "name", searchString, undefined, asUser) + .then(resp => { + setTotal(resp.data.total); + resp.data.teams.forEach((team, index) => { + let u = asUser ? asUser : user.id(); + if (team.settings && u !== "") { + userAPI + .getUser(u, teamScope(team.settings?.key)) + .then(resp => { + newTeamPerms[index] = resp.permissions; + newTeamPrefs[index] = resp.preferences; + }) + .finally(() => { + if (newTeamPerms[index]) setTeamPerms([...newTeamPerms]); + if (newTeamPrefs[index]) setTeamPrefs(newTeamPrefs); + }); + } + }); + setTeams(resp.data.teams); + }) + .finally(() => { + setLoading(false); + }); + }, [user, teamAPI, userAPI, limit, page, search, asUser]); + + useEffect(() => { + loadTeams(); + }, [user, teamAPI, userAPI, limit, page, search, asUser, loadTeams]); + + const drawTeams = () => { + return ( + + {teams.map((team, index) => { + let permissions = teamPerms[index]; + if (!permissions) { + permissions = []; + } + let preferences = teamPrefs[index]; + if (!preferences) { + preferences = pond.TeamPreferences.create(); + } + return ( + + + + { + if (team.settings) history("teams/" + team.settings.key); + }} + src={team.settings?.avatar} + alt="" + className={classes.clickable} + /> + + { + if (team.settings) history("teams/" + team.settings.key); + }}> + {team.settings?.name} + + } + secondary={ + + + {team.settings?.info} + + + } + /> + {/* + { + if (preferences) { + let newTeamPrefs = cloneDeep(teamPrefs); + newTeamPrefs[index].notify = !newTeamPrefs[index].notify; + console.log(newTeamPrefs[index]); + teamAPI + .updatePreferences( + Team.create(team).key(), + newTeamPrefs[index], + getContextKeys(), + getContextTypes() + ) + .then(resp => { + setTeamPrefs(newTeamPrefs); + }); + // setTeamPrefs(newTeamPrefs) + } + }} + /> + */} + + + + ); + })} + + ); + }; + + const showProgress = () => { + return ( +
+ +
+ ); + }; + + const firstPage = () => { + setPage(0); + }; + + const lastPage = () => { + setPage(Math.round(total / limit)); + }; + + const nextPage = () => { + if (limit * (page + 1) < total) setPage(page + 1); + }; + + const prevPage = () => { + if (page > 0) setPage(page - 1); + }; + + const handleChange = (event: any) => { + setLimit(event.target.value); + }; + + const showPageButtons = () => { + return ( +
+ Teams per page: + + {limits.map(option => ( + + {option.label} + + ))} + + + + + + + + + {limit * page + 1} ‐ {limit * page + limit <= limit ? limit * page + limit : limit + 1} of{" "} + {total} + + = total} + size="small" + color="inherit" + onClick={nextPage}> + + + = total} + size="small" + color="inherit" + onClick={lastPage}> + + +
+ ); + }; + + return ( + +
+ + {isUser ? user.name() + "'s " : "Someone's "} Teams + + setTeamDialogOpen(true)} /> + setSearch(event?.target.value)} + InputProps={{ + startAdornment: ( + + + + ) + }}> +
+ + {isUser ? "Viewing all teams that you are a part of" : "These are someone else's teams"} + + {!loading ? drawTeams() : showProgress()} + {showPageButtons()} + setTeamDialogOpen(false)} + refreshCallback={loadTeams} + /> +
+ ); +} diff --git a/src/teams/TeamSettings.tsx b/src/teams/TeamSettings.tsx new file mode 100644 index 0000000..60925fd --- /dev/null +++ b/src/teams/TeamSettings.tsx @@ -0,0 +1,265 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Tab, + Tabs, + TextField, + Typography, + useTheme, +} from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { Team } from "models"; +import { pond } from "protobuf-ts/pond"; +import { useSnackbar, useTeamAPI } from "providers"; +import React, { useEffect, useState } from "react"; +import DeleteButton from "common/DeleteButton"; +import { userRoleFromPermissions } from "pbHelpers/User"; +import { IconPicker } from "common/IconPicker"; + +interface Props { + teamDialogOpen: boolean; + closeTeamDialogCallback: () => void; + refreshCallback?: () => void; + team?: Team; + permissions?: pond.Permission[]; +} + +export default function TeamSettings(props: Props) { + const theme = useTheme(); + const teamAPI = useTeamAPI(); + const { teamDialogOpen, closeTeamDialogCallback, permissions, refreshCallback } = props; + const [loading, setLoading] = useState(false); + const [nameField, setNameField] = useState(""); + const [infoField, setInfoField] = useState(""); + const [url, setUrl] = useState(""); + const snackbar = useSnackbar(); + const [tab, setTab] = useState(0); + const [team, setTeam] = useState(); + const [teamKey, setTeamKey] = useState(); + const [openDelete, setOpenDelete] = useState(false); + + useEffect(() => { + if (props.team) { + setTeam(props.team); + setTeamKey(props.team.key()); + } + }, [props.team]); + + useEffect(() => { + if (team) { + setNameField(team.name()); + setInfoField(team.settings.info); + setUrl(team.settings.avatar); + } + }, [team]); + + // const ColorButton = withStyles((theme: Theme) => ({ + // root: { + // color: theme.palette.getContrastText(theme.palette.primary.main), + // backgroundColor: theme.palette.primary.main, + // "&:hover": { + // backgroundColor: theme.palette.primary.light + // }, + // "&:disabled": { + // backgroundColor: "rgba(0,0,0,0)", + // color: theme.palette.getContrastText(theme.palette.primary.main) + // } + // } + // }))(Button); + + const removeTeam = () => { + if (team?.key()) + teamAPI + .removeTeam(team?.key()) + .then(() => { + snackbar.info("Team removed successfully."); + if (refreshCallback) refreshCallback(); + closeTeamDialogCallback(); + }) + .catch(err => { + console.log(err); + }); + }; + + const addTeam = () => { + let team = pond.TeamSettings.create(); + team.name = nameField; + team.info = infoField; + setLoading(true); + teamAPI + .addTeam(team) + .then(resp => { + snackbar.success("Team added successfully!"); + setTab(1); + let t = pond.Team.create(); + t.settings = team; + setTeam(Team.create(t)); + setTeamKey(resp.data.team); + if (refreshCallback) refreshCallback(); + }) + .catch(err => { + snackbar.error(err); + }) + .finally(() => { + setLoading(false); + }); + }; + + const updateTeam = () => { + let newTeam = pond.TeamSettings.create(); + newTeam.name = nameField; + newTeam.info = infoField; + newTeam.avatar = url; + if (teamKey) { + teamAPI + .updateTeam(teamKey, newTeam) + .then(() => { + snackbar.success("Team update successfully!"); + }) + .catch(err => { + snackbar.error(err); + }) + .finally(() => { + if (refreshCallback) refreshCallback(); + setNameField(""); + setInfoField(""); + setUrl(""); + setTeamKey(undefined); + setTab(0); + closeTeamDialogCallback(); + }); + } + }; + + const disableButton = () => { + if (loading) return true; + if (nameField === "" || infoField === "") return true; + }; + + const close = () => { + setNameField(""); + setInfoField(""); + setUrl(""); + closeTeamDialogCallback(); + }; + + const isOwner = () => { + if (permissions) { + return userRoleFromPermissions(permissions) === "Owner"; + } + return false; + }; + + const deleteConfirmation = () => { + return ( + { + setOpenDelete(false); + }}> + Delete Team + + + Performing this action will delete this team and and any file attachments associated + with it. + + Are you sure you wish to continue. + + + + Confirm + + + ); + }; + + const teamSettings = () => { + return ( + + + Add more info about your team, business, or company. You can have multiple teams if + needed. + + setNameField(event?.target.value)} + variant="outlined" + style={{ margin: theme.spacing(1) }} + fullWidth + /> +
+ setInfoField(event?.target.value)} + variant="outlined" + style={{ margin: theme.spacing(1) }} + fullWidth + /> + + ); + }; + + let disable = disableButton(); + return ( + + + {team ? "Update " + team.name() + "?" : "Create New Team?"} + + setTab(value)} + aria-label="basic tabs example" + centered> + + + + + {tab === 0 && teamSettings()} + {tab === 1 && teamKey !== undefined && ( + + + + )} + + {isOwner() && ( + { + setOpenDelete(true); + }} + aria-label="Cancel" + style={{ marginRight: "auto", marginLeft: theme.spacing(2) }}> + Delete + + )} + + + + + {deleteConfirmation()} + + ); +} diff --git a/src/user/ObjectUsers.tsx b/src/user/ObjectUsers.tsx new file mode 100644 index 0000000..eef771f --- /dev/null +++ b/src/user/ObjectUsers.tsx @@ -0,0 +1,719 @@ +import { + Avatar, + Box, + Button, + Checkbox, + CircularProgress, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + Grid, + Grid2, + IconButton, + List, + ListItem, + ListItemAvatar, + ListItemSecondaryAction, + ListItemText, + PaletteColor, + Stack, + Theme, + Tooltip, + Typography, + useTheme +} from "@mui/material"; +// import { Theme } from "@material-ui/core/styles/createMuiTheme"; +// import { PaletteColor } from "@material-ui/core/styles/createPalette"; +import RemoveUserIcon from "@mui/icons-material/RemoveCircle"; +import EditIcon from "@mui/icons-material/EditOutlined"; +import ShareIcon from "@mui/icons-material/Share"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { usePermissionAPI, usePrevious, useSnackbar, useUserAPI } from "hooks"; +import { cloneDeep, isEqual } from "lodash"; +import { Scope, User } from "models"; +import { sortUsersByRole, userRoleFromPermissions } from "pbHelpers/User"; +import { pond } from "protobuf-ts/pond"; +import React, { useCallback, useEffect, useState } from "react"; +import { or } from "utils/types"; +import RemoveSelfFromObject from "user/RemoveSelfFromObject"; +import ShareObject from "user/ShareObject"; +import { useGlobalState } from "providers"; +import { red, blue } from "@mui/material/colors"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => { + const avatarBG = theme.palette.secondary["700" as keyof PaletteColor]; + return { + dialogContent: { + padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`, + [theme.breakpoints.up("sm")]: { + padding: `${theme.spacing(1)}px ${theme.spacing(3)}px` + } + }, + userAvatar: { + color: "#fff", + backgroundColor: avatarBG + }, + userPermissions: { + paddingLeft: theme.spacing(4), + paddingBottom: theme.spacing(1) + }, + textOverflowEllipsis: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis" + }, + removeIcon: { + color: red["500"], + "&:hover": { + color: red["600"] + } + }, + verticalSpacing: { + paddingTop: theme.spacing(4), + paddingBottom: theme.spacing(3) + }, + lessSpacing: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1) + }, + iconButton: { + margin: theme.spacing(1) + }, + shareIcon: { + color: blue["600"], + "&:hover": { + color: blue["700"] + } + }, + editIcon: { + color: blue["300"], + "&:hover": { + color: blue["400"] + } + } + }; +}); + +interface Props { + scope: Scope; + label: string; + permissions: pond.Permission[]; + isDialogOpen: boolean; + closeDialogCallback: Function; + refreshCallback: Function; + userCallback?: (users?: User[] | undefined) => void; + dialog?: string; + cardMode?: boolean; + useImitation?: boolean; +} + +export default function ObjectUsers(props: Props) { + const classes = useStyles(); + const theme = useTheme(); + const permissionAPI = usePermissionAPI(); + const [{ user, team }, dispatch] = useGlobalState(); + const canProvision = user.allowedTo("provision"); + const userAPI = useUserAPI(); + const { error, success, warning } = useSnackbar(); + const { + scope, + label, + permissions, + isDialogOpen, + closeDialogCallback, + refreshCallback, + userCallback, + dialog, + cardMode + //useImitation + } = props; + const prevPermissions = usePrevious(permissions); + const prevIsDialogOpen = usePrevious(isDialogOpen); + const [initialUsers, setInitialUsers] = useState([]); + const [users, setUsers] = useState([]); + const [canManageUsers, setCanManageUsers] = useState( + permissions.includes(pond.Permission.PERMISSION_USERS) + ); + const [canShare, setCanShare] = useState( + permissions.includes(pond.Permission.PERMISSION_SHARE) + ); + const [canShareBilling, setCanShareBilling] = useState( + permissions.includes(pond.Permission.PERMISSION_SHARE) && + permissions.includes(pond.Permission.PERMISSION_USERS) && + permissions.includes(pond.Permission.PERMISSION_WRITE) + ); + const [removedUsers, setRemovedUsers] = useState([]); + const [removeSelfDialogIsOpen, setRemoveSelfDialogIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isShareObjectDialogOpen, setIsShareObjectDialogOpen] = useState(false); + const [numOwners, setNumOwners] = useState(0); + const [useImitation] = useState( + props.useImitation !== undefined ? props.useImitation : true + ); + // const [editUserDialog, setEditUserDialog] = useState(false); + const [userForDialog, setUserForDialog] = useState(undefined); + + const setDefaultState = () => { + setInitialUsers([]); + setUsers([]); + setRemovedUsers([]); + }; + + const load = useCallback(() => { + setIsLoading(true); + userAPI + .listObjectUsers(scope) + .then((response: any) => { + let rUsers: User[] = []; + or(response.data, { users: [] }).users.forEach((user: any) => { + rUsers.push(User.any(user)); + }); + rUsers = rUsers.filter(u => u.permissions.length > 0); + setInitialUsers(cloneDeep(rUsers)); + let numOwners = 0; + rUsers.forEach(user => { + if (userRoleFromPermissions(user.permissions) === "Owner") numOwners++; + }); + setNumOwners(numOwners); + setUsers(rUsers); + }) + .catch((err: any) => { + console.log(err); + setInitialUsers([]); + setUsers([]); + }) + .finally(() => setIsLoading(false)); + }, [scope, userAPI]); + + useEffect(() => { + if (!prevIsDialogOpen && isDialogOpen) { + load(); + } + if (prevPermissions !== permissions) { + setCanManageUsers(permissions.includes(pond.Permission.PERMISSION_USERS)); + setCanShare(permissions.includes(pond.Permission.PERMISSION_SHARE)); + setCanShareBilling( + permissions.includes(pond.Permission.PERMISSION_SHARE) && + permissions.includes(pond.Permission.PERMISSION_USERS) && + permissions.includes(pond.Permission.PERMISSION_WRITE) + ); + } + }, [isDialogOpen, load, permissions, prevIsDialogOpen, prevPermissions]); + + const closeRemoveSelfDialog = () => { + setRemoveSelfDialogIsOpen(false); + }; + + const close = () => { + closeRemoveSelfDialog(); + closeDialogCallback(); + setDefaultState(); + }; + + const submit = () => { + console.log(users); + permissionAPI + .updatePermissions(scope, users) + .then((response: any) => { + success("Users were sucessfully updated for " + label); + close(); + refreshCallback(); + userCallback && userCallback(users); + users.forEach(u => { + if (u.id() === user.id()) { + if (scope.kind === "team" && scope.key === team.key()) { + dispatch({ key: "userTeamPermissions", value: u.permissions }); + } + } + }); + if (cardMode) { + load(); + } + }) + .catch((err: any) => { + err.response.data.error + ? warning(err.response.data.error) + : error("Error occured when updating users for " + label); + close(); + }) + .finally(() => setUserForDialog(undefined)); + }; + + const changeUserPermissions = (user: pond.IUser) => (event: any) => { + let updatedUsers = cloneDeep(users); + let permissionMapping = new Map([ + ["2", pond.Permission.PERMISSION_READ], + ["3", pond.Permission.PERMISSION_WRITE], + ["4", pond.Permission.PERMISSION_SHARE], + ["1", pond.Permission.PERMISSION_USERS], + ["5", pond.Permission.PERMISSION_BILLING], + ["6", pond.Permission.PERMISSION_FILE_MANAGEMENT] + ]); + let permission = or( + permissionMapping.get(event.target.value), + pond.Permission.PERMISSION_INVALID + ); + for (let i = 0; i < updatedUsers.length; i++) { + let currUser = updatedUsers[i]; + if ( + user && + user.settings && + currUser && + currUser.settings && + currUser.settings.id === user.settings.id + ) { + let permissions = user.permissions ? cloneDeep(user.permissions) : []; + if (permissions.includes(permission)) { + permissions = permissions.filter( + (curPermission: pond.Permission) => curPermission !== permission + ); + } else { + permissions.push(permission); + } + updatedUsers[i].permissions = permissions; + user.permissions = permissions; + break; + } + } + setUsers(updatedUsers); + }; + + const removeUser = (user: string) => { + let updatedUsers = cloneDeep(users); + let updatedRemovedUsers = cloneDeep(removedUsers); + for (let i = 0; i < updatedUsers.length; i++) { + let currUser = updatedUsers[i]; + if (currUser && currUser.settings && currUser.settings.id === user) { + updatedRemovedUsers.push(currUser.settings.id); + updatedUsers[i].permissions = []; + break; + } + } + setUsers(updatedUsers); + setRemovedUsers(updatedRemovedUsers); + }; + + const checkIsSelf = (checkUser: pond.IUser): boolean => { + const id = checkUser && checkUser.settings ? checkUser.settings.id : ""; + return user.id() === id; + }; + + const userIsRemoved = (user: pond.IUser): boolean => { + const id = user && user.settings ? user.settings.id : ""; + return removedUsers.includes(or(id, "")); + }; + + const objectUsersUnchanged = (): boolean => { + return isEqual(initialUsers, users); + }; + + const openRemoveSelfDialog = () => { + setRemoveSelfDialogIsOpen(true); + }; + + const openShareObjectDialog = () => { + setIsShareObjectDialogOpen(true); + }; + + const closeShareObjectDialog = (shared: boolean | undefined) => { + setIsShareObjectDialogOpen(false); + if (shared) { + load(); + } + }; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + const title = () => { + return ( + // + // + // + // Users + // + // {label} + // + // + // + // {canShare && ( + // + // + // + // + // + // )} + // + // + // {dialog && ( + // + // {dialog} + // + // )} + // + + + + Users + + {label} + + + + {canShare && ( + + + + + + )} + + + {dialog && ( + + {dialog} + + )} + + ); + }; + + const userPermissionManagement = (user: User) => { + let permissions = user.permissions; + if ( + !canManageUsers || + (checkIsSelf(user) && !canShareBilling) || + userIsRemoved(user) || + (permissions.includes(pond.Permission.PERMISSION_USERS) && + permissions.includes(pond.Permission.PERMISSION_READ) && + permissions.includes(pond.Permission.PERMISSION_WRITE) && + permissions.includes(pond.Permission.PERMISSION_SHARE) && + permissions.includes(pond.Permission.PERMISSION_BILLING) && + permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) && + !cardMode) + ) { + return null; + } + const canRead = permissions.includes(pond.Permission.PERMISSION_READ); + const xs = cardMode ? 12 : 4; + const sm = cardMode ? 4 : 3; + const justify = cardMode ? "flex-start" : "center"; + return ( + + + + + + + } + label="View" + labelPlacement="end" + /> + + + + + } + label="Edit" + labelPlacement="end" + /> + + + + + } + label="Share" + labelPlacement="end" + /> + + + + + } + label="Manage Users" + labelPlacement="end" + /> + + + + + } + label="Files" + labelPlacement="end" + /> + + + + + } + label="Billing" + labelPlacement="end" + /> + + + + + ); + }; + + const userListItem = (user: User) => { + let isSelf = checkIsSelf(user); + let isRemoved = userIsRemoved(user); + let name = user.name(); + let permissions = user.permissions; + let userSettings = pond.UserSettings.create(user.settings !== null ? user.settings : undefined); + + return ( + + + + {!(userSettings.avatar && userSettings.avatar !== "") && name} + + + + {name + (isSelf ? " (you)" : "")} + + } + secondary={ + + + {user.settings.email} + + + {isRemoved ? "REMOVED " : userRoleFromPermissions(permissions)} + + + } + /> + {isSelf && !canShareBilling ? ( + + + + + + + + ) : (canManageUsers || canProvision) && !isRemoved && !cardMode ? ( + + + removeUser(or(user.settings, { id: "" }).id)}> + + + + + ) : ( + (canManageUsers || canProvision) && ( + + + setUserForDialog(user)}> + + + + + ) + )} + + ); + }; + + const objectUsersList = () => { + const sortedUsers = sortUsersByRole(users); + + let userListItems: any = []; + for (var i = 0; i < sortedUsers.length; i++) { + let user = sortedUsers[i]; + if (user) { + userListItems.push( + + {userListItem(user)} + {!cardMode && userPermissionManagement(user)} + + + ); + } + } + + return {userListItems}; + }; + + const content = () => { + if (isLoading) { + return ( + + + + ); + } + + if (users.length < 1) { + return ( + + + No users found. + + + ); + } + + return objectUsersList(); + }; + + const actions = () => { + return ( + + + + {!cardMode && ( + + )} + {canManageUsers && !cardMode && ( + + )} + + + ); + }; + + const dialogs = () => { + return ( + + + + + ); + }; + + if (cardMode) { + return ( + + {title()} +
+ + {content()} + {actions()} + {dialogs()} +
+
+ setUserForDialog(undefined)}> + Change {userForDialog?.name()}'s Permissions? + {userForDialog && userPermissionManagement(userForDialog)} + + + + + + + ); + } + + return ( + + {title()} + + {content()} + {actions()} + {dialogs()} + + ); +} diff --git a/src/user/RemoveSelfFromObject.tsx b/src/user/RemoveSelfFromObject.tsx new file mode 100644 index 0000000..f05d82a --- /dev/null +++ b/src/user/RemoveSelfFromObject.tsx @@ -0,0 +1,111 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography +} from "@mui/material"; +import { Scope, User } from "models"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState, usePermissionAPI, useSnackbar } from "providers"; +import React from "react"; +import { useNavigate } from "react-router"; + +interface Props { + scope: Scope; + label: string; + isDialogOpen: boolean; + closeDialogCallback: (removed: boolean) => void; + path?: string; + numberOfOwners?: number; //including the number of owners will prevent the user from leaving if they are the final owner +} + +export default function RemoveSelfFromObject(props: Props) { + const navigate = useNavigate(); + const permissionAPI = usePermissionAPI(); + const [{ user }] = useGlobalState(); + const { error, success, warning } = useSnackbar(); + const { scope, label, isDialogOpen, closeDialogCallback, path, numberOfOwners } = props; + const userID = user.id() + + const close = (removed: boolean) => { + closeDialogCallback(removed); + }; + + const submit = () => { + permissionAPI + .updatePermissions(scope, [ + User.create( + pond.User.create({ + settings: pond.UserSettings.create({ + id: userID + }), + permissions: [] + }) + ) + ]) + .then((response: any) => { + success("Successfully left " + label); + close(true); + navigate("/" + (path !== undefined ? path : scope.kind) + "s"); + }) + .catch((err: any) => { + err.response.data.error + ? warning(err.response.data.error) + : error("Error occured when trying to remove yourself from " + label); + close(false); + }); + }; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI BEGINS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + const removeSelfDialog = () => { + return ( + + {"Leave " + label + "?"} + + + You will no longer be able to access {label}. + + + + + + + + ); + }; + + const onlyOwnerDialog = () => { + return ( + + {"Cannot Leave"} + + + You are currently the only owner. To leave, share it to another user with full + permissions to make them an owner first and then leave. + + + + + + + ); + }; + + return ( + close(false)} + aria-labelledby="remove-self-from-object"> + {numberOfOwners && numberOfOwners <= 1 ? onlyOwnerDialog() : removeSelfDialog()} + + ); +} diff --git a/src/user/ShareObject.tsx b/src/user/ShareObject.tsx new file mode 100644 index 0000000..f79b651 --- /dev/null +++ b/src/user/ShareObject.tsx @@ -0,0 +1,601 @@ +import { + Button, + Checkbox, + CircularProgress, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + IconButton, + Link, + List, + ListItem, + ListItemSecondaryAction, + ListItemText, + Tab, + Tabs, + TextField, + Theme, + Tooltip, + Typography +} from "@mui/material"; +import { green, red } from "@mui/material/colors"; +// import { Theme } from "@mui/material/styles/createMuiTheme"; +import { + Add as AddIcon, + Link as LinkIcon, + LinkOff, + RemoveCircle as RemoveCircleIcon +} from "@mui/icons-material"; +// import { MobileDateTimePicker } from "@mui/utils/pi"; +import { MobileDateTimePicker } from '@mui/x-date-pickers'; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { usePermissionAPI, usePrevious, useSnackbar } from "hooks"; +import { cloneDeep } from "lodash"; +import { binScope, Scope, ShareableLink, User } from "models"; +import moment, { Moment } from "moment"; +import { pond } from "protobuf-ts/pond"; +import React, { useCallback, useEffect, useState } from "react"; +import { openSnackbar } from "providers/Snackbar"; +// import { Status } from "@sentry/react"; +import { useBinAPI, useGateAPI } from "providers"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + dialogContent: { + marginTop: theme.spacing(2) + }, + formContainer: { + display: "flex", + flexWrap: "wrap" + }, + formControl: { + margin: theme.spacing(1), + minWidth: 160, + width: "100%" + }, + emailForm: { + paddingBottom: theme.spacing(2) + }, + fabContainer: { + position: "relative", + minHeight: "64px" + }, + addIcon: { + color: green[500], + "&:hover": { + color: green[600] + } + }, + removeIcon: { + color: red[500], + "&:hover": { + color: red[600] + } + }, + grey: { + color: theme.palette.text.secondary + } +})); + +interface Props { + scope: Scope; + label: string; + permissions: pond.Permission[]; + isDialogOpen: boolean; + closeDialogCallback: Function; + useImitation?: boolean; + sharedUsers?: User[]; //including the users that the object/team is already shared to will prevent users from sharing again to an existing user +} + +export default function ShareObject(props: Props) { + const binAPI = useBinAPI(); + const classes = useStyles(); + const permissionAPI = usePermissionAPI(); + const { info, success, error } = useSnackbar(); + const { scope, label, permissions, isDialogOpen, closeDialogCallback, sharedUsers } = props; + const [email, setEmail] = useState(""); + const [sharedPermissions, setSharedPermissions] = useState([ + pond.Permission.PERMISSION_READ + ]); + const [tab, setTab] = useState(0); + const prevTab = usePrevious(tab); + const [isLoadingLinks, setIsLoadingLinks] = useState(false); + const [shareableLinks, setShareableLinks] = useState([]); + const [newLinkExpiration, setNewLinkExpiration] = useState(moment().add(1, "week")); + const [isNewLinkInfinite, setIsNewLinkInfinite] = useState(false); + const [openExistingUser, setOpenExistingUser] = useState(false); + const [useImitation] = useState( + props.useImitation !== undefined ? props.useImitation : true + ); + const gateAPI = useGateAPI(); + + const share = () => { + permissionAPI + .shareObject(scope, email, sharedPermissions, useImitation) + .then((result: any) => { + let shareBins = true; + if (result && result.data && result.data.existing) { + success(label + " was shared with " + email); + } else { + success(email + " was sent an email to sign up"); + shareBins = false; + } + let successBins = true; + if (scope.kind === "binyard" && shareBins) { + binAPI.listBins(1000, 0, "asc", "name", scope.key).then(resp => { + resp.data.bins.forEach(bin => { + if (bin.settings) { + let newScope = binScope(bin.settings?.key); + permissionAPI.shareObject(newScope, email, sharedPermissions).catch(err => { + successBins = false; + }); + } + }); + }); + if (!successBins) { + openSnackbar("error", "One or more bins failed to share with " + email); + } else { + success("Bins shared with " + email); + + } + } + let successGates = true; + if (scope.kind === "terminal" && shareBins) { + gateAPI + .listGates( + 1000, + 0, + undefined, + undefined, + undefined, + undefined, + [scope.key], + [scope.kind] + ) + .then(resp => { + resp.data.gates.forEach(gate => { + if (gate) { + let newScope = { key: gate.key, kind: "gate" } as Scope; + permissionAPI.shareObject(newScope, email, sharedPermissions).catch(err => { + successGates = false; + }); + } + }); + }); + if (!successGates) { + openSnackbar("error", "One or more Gates failed to share"); + } else { + success("Gates shared"); + } + } + closeAfterShare(); + }) + .catch((err: any) => { + error("Unable to share " + label + " with " + email); + close(); + }); + }; + + const loadShareableLinks = useCallback(() => { + setIsLoadingLinks(true); + permissionAPI + .listShareableLinks(scope) + .then((response: any) => { + let rawShareableLinks = response.data.links ? response.data.links : []; + let rShareableLinks = rawShareableLinks.map((rawShareableLink: any) => + ShareableLink.any(rawShareableLink) + ); + setShareableLinks(rShareableLinks); + }) + .catch((error: any) => { + setShareableLinks([]); + console.log("Error occured while loading shareable links"); + }) + .finally(() => { + setIsLoadingLinks(false); + }); + }, [permissionAPI, scope]); + + useEffect(() => { + if (tab === 1 && prevTab !== tab) { + loadShareableLinks(); + } + }, [loadShareableLinks, prevTab, tab]); + + const getNeverExpires = () => { + return ""; + }; + + const createShareableLink = () => { + const expiration = isNewLinkInfinite ? getNeverExpires() : newLinkExpiration.toISOString(); + permissionAPI + .addShareableLink(scope, expiration) + .then((response: any) => { + loadShareableLinks(); + }) + .catch((error: any) => { + console.log("Error occured while creating shareable link"); + }); + }; + + const revokeShareableLink = (code: string) => { + permissionAPI + .removeShareableLink(scope, code) + .then((response: any) => { + loadShareableLinks(); + }) + .catch((error: any) => { + console.log("Error occured while removing the shareable link"); + }); + }; + + const getShareableLinkURL = (code: string): string => { + const base = window.location.origin; + return base + "/" + scope.kind + "s/" + code; + }; + + const copyShareableLinkToClipboard = (code: string) => { + const url = getShareableLinkURL(code); + navigator.clipboard.writeText(url); + info("Copied link to clipboard"); + }; + + const closeAfterShare = () => { + closeDialogCallback(true); + }; + + const close = () => { + closeDialogCallback(false); + }; + + const changeEmail = (event: any) => { + setEmail(event.target.value); + }; + + const changePermissions = (event: any) => { + let updatedSharedPermissions: Array = cloneDeep(sharedPermissions); + let permission: pond.Permission; + switch (event.target.value) { + case "1": + permission = pond.Permission.PERMISSION_USERS; + break; + case "2": + permission = pond.Permission.PERMISSION_READ; + break; + case "3": + permission = pond.Permission.PERMISSION_WRITE; + break; + case "4": + permission = pond.Permission.PERMISSION_SHARE; + break; + case "5": + permission = pond.Permission.PERMISSION_BILLING; + break; + case "6": + permission = pond.Permission.PERMISSION_FILE_MANAGEMENT; + break; + default: + permission = pond.Permission.PERMISSION_INVALID; + break; + } + + if (updatedSharedPermissions.includes(permission)) { + updatedSharedPermissions = updatedSharedPermissions.filter( + (curPermission: pond.Permission) => curPermission !== permission + ); + } else { + updatedSharedPermissions.push(permission); + } + + setSharedPermissions(updatedSharedPermissions); + }; + + const changeTab = (event: any, newTab: number) => { + setTab(newTab); + }; + + const isValid = () => { + return email.trim() !== "" && sharedPermissions.length > 0; + }; + + // UI BEGINS + + const emailForm = () => { + return ( + + ); + }; + + const permissionsForm = () => { + return ( + + Permissions + + {permissions.includes(pond.Permission.PERMISSION_READ) && ( + + } + label="View" + labelPlacement="end" + /> + )} + + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + } + label="Edit" + labelPlacement="end" + /> + )} + + {permissions.includes(pond.Permission.PERMISSION_SHARE) && ( + + } + label="Share" + labelPlacement="end" + /> + )} + + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + + } + label="Manage Users" + labelPlacement="end" + /> + )} + + {permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT) && ( + + } + label="Files" + labelPlacement="end" + /> + )} + + {permissions.includes(pond.Permission.PERMISSION_BILLING) && ( + + } + label="Billing" + labelPlacement="end" + /> + )} + + + ); + }; + + const emailSharing = () => { + return ( + + {emailForm()} + {permissionsForm()} + + ); + }; + + const existingUserDialog = () => { + return ( + { + setOpenExistingUser(false); + close(); + }}> + User Already exists + This user is already part of {label}. + + + + + ); + }; + + const shareableLinksList = () => { + const now = moment(); + let items: any[] = []; + shareableLinks.forEach((link: ShareableLink) => { + const expiration = moment(link.settings.expiration); + const neverExpires = link.settings.expiration === getNeverExpires(); + const expired = !neverExpires && expiration < now; + items.push( + + {expired ? ( + + + + ) : ( + + copyShareableLinkToClipboard(link.key())}> + + + + )} + + {"/devices/" + link.key()} + ) : ( + + {"/devices/" + link.key()} + + ) + } + secondary={ + neverExpires ? ( + "Never expires" + ) : ( + + {(expired ? "Expired " : "Expires ") + expiration.fromNow()} + + ) + } + /> + + + revokeShareableLink(link.key())}> + + + + + + ); + }); + return ( + + {isLoadingLinks ? : {items}} + {shareableLinks.length > 0 && } + + + } + label="Expiration Date" + value={newLinkExpiration} + onChange={date => setNewLinkExpiration(date as Moment)} + disablePast + /> + setIsNewLinkInfinite(checked)} + value="isNewLinkInfinite" + color="secondary" + /> + } + label="Never expires" + /> + + + + createShareableLink()}> + + + + + + + ); + }; + + const content = () => { + switch (tab) { + case 1: + return {shareableLinksList()}; + default: + return {emailSharing()}; + } + }; + + return ( + + {existingUserDialog()} + + + Share + + {label} + + + {scope.kind === "device" && ( + + + + + )} + + {content()} + + + {tab === 0 && ( + + )} + + + + ); +} diff --git a/src/utils/environment.ts b/src/utils/environment.ts new file mode 100644 index 0000000..8de773c --- /dev/null +++ b/src/utils/environment.ts @@ -0,0 +1,32 @@ +function getEnvironment() { + switch (process.env.NODE_ENV) { + case "development": + return "development"; + case "production": + if (window.location.hostname.includes("staging")) { + return "staging"; + } else { + return "production"; + } + case "test": + return "test"; + default: + return ""; + } +} + +export function isOffline() { + return process.env.REACT_APP_IS_OFFLINE === "true"; +} + +export function isDevelopment() { + return getEnvironment() === "development"; +} + +export function isProduction() { + return getEnvironment() === "production"; +} + +export function isStaging() { + return getEnvironment() === "staging"; +}