diff --git a/.env b/.env index 9fdc8f3..90ef699 100644 --- a/.env +++ b/.env @@ -14,6 +14,7 @@ VITE_AUTH0_ADAPTIVE_CONSTRUCTION_CLIENT_ID=32rABabJzXRvJiWivTmeKFgwFiqh4ok7 VITE_AUTH0_AEROGROW_CLIENT_ID=KHl9ooUt1nia1RYw5n224dyggCXdbsSd VITE_AUTH0_MIVENT_CLIENT_ID=VNALE7RW6l3dY5uYcxgwElZV0lcT25Fg VITE_AUTH0_OMNIAIR_CLIENT_ID=IblmarD8wFafiD6doxTmOHQ6Bx3L9wWl +VITE_AUTH0_INTELLIFARMS_CLIENT_ID=RYtuAyOcB4DSaaqJMLDMf3pV8SFY9PdY #Branding (Default theme) VITE_APP_WEBSITE_TITLE="Adaptive Dashboard" diff --git a/README.md b/README.md index 8528fdc..4a47964 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,10 @@ Steps to add a new white label - add a `CNAME` record pointing their custom subdomain (ex: dashboard.example.com) to brandxtech.ca - add a login button on their website point to `/login` of their custom subdomain (ex: dashboard.example.com/login) +It is recommended to start with the Auth0 stage once everything from the client has been recieved as things like the client ID will be needed in other stages + +Frontend Steps 1. Create a PWA folder in `public` that is named after the client and generate PWA assets using [this](https://realfavicongenerator.net/) website (use the company logo but you might need to use a custom favicon depending on the result) - https://maskable.app/ also create a maskable icon for PWA @@ -29,22 +32,23 @@ Steps to add a new white label 4. If there are any pages specific to the whitelabel be sure to update the side and bottom navigator +Backend Steps 1. In `dynamic-config.prod.yml`, add a router entry for the new whitelabel 2. In `pond/http/server.go`, add the sub-domain to the `allowedOrigins` 3. In `pond/whitelabel.go`, add an entry for the new white label -4. Enable the white-label sub-domain for CORS in the backend by adding it to the allowed origins http/server.go -1. Create a new `Auth0` application for the client +Auth0 Steps +1. Create a new Single Page App on Auth0 using React as the tech +2. Once you have the client ID add it to your .env file in the frontend +3. Adjust settings accordingly in the new application - use a image bucket like [postimg](https://postimages.org/) to store their company logo and favicon - add a whitelabel logo - - allow necessary `origins`, `callback URLs`, and `CORS domains` to the application settings (see the default application as a guideline) + - allow necessary `origins`, `callback URLs`, and `CORS domains` to the application settings (see other applications/whitelabels as a guideline) - customize the Universal Login page to handle the new whitelabel - check if the client ID matches this client's Auth0 application - change the Universal Login theme using white-label colours and images stored in a bucket (ex: [postimg](https://postimages.org/)) -- in [http.go](https://gitlab.com/brandx/backend/blob/master/pond/http.go), add the white-label sub-domain to the `AllowedOrigins` list - ## Expanding the ESLint configuration If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: diff --git a/public/Intellifarms/IFND-2023-Logo-White.png b/public/Intellifarms/IFND-2023-Logo-White.png new file mode 100644 index 0000000..0afb3bf Binary files /dev/null and b/public/Intellifarms/IFND-2023-Logo-White.png differ diff --git a/public/Intellifarms/IFND-2023-Logo.png b/public/Intellifarms/IFND-2023-Logo.png new file mode 100644 index 0000000..11ff1c8 Binary files /dev/null and b/public/Intellifarms/IFND-2023-Logo.png differ diff --git a/public/Intellifarms/android-chrome-192x192.png b/public/Intellifarms/android-chrome-192x192.png new file mode 100644 index 0000000..11f1839 Binary files /dev/null and b/public/Intellifarms/android-chrome-192x192.png differ diff --git a/public/Intellifarms/android-chrome-512x512.png b/public/Intellifarms/android-chrome-512x512.png new file mode 100644 index 0000000..e4f7470 Binary files /dev/null and b/public/Intellifarms/android-chrome-512x512.png differ diff --git a/public/Intellifarms/apple-touch-icon.png b/public/Intellifarms/apple-touch-icon.png new file mode 100644 index 0000000..408dde9 Binary files /dev/null and b/public/Intellifarms/apple-touch-icon.png differ diff --git a/public/Intellifarms/browserconfig.xml b/public/Intellifarms/browserconfig.xml new file mode 100644 index 0000000..f62a123 --- /dev/null +++ b/public/Intellifarms/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #ffffff + + + diff --git a/public/Intellifarms/favicon-16x16.png b/public/Intellifarms/favicon-16x16.png new file mode 100644 index 0000000..f052e75 Binary files /dev/null and b/public/Intellifarms/favicon-16x16.png differ diff --git a/public/Intellifarms/favicon-32x32.png b/public/Intellifarms/favicon-32x32.png new file mode 100644 index 0000000..460f109 Binary files /dev/null and b/public/Intellifarms/favicon-32x32.png differ diff --git a/public/Intellifarms/favicon-48x48.png b/public/Intellifarms/favicon-48x48.png new file mode 100644 index 0000000..948fec7 Binary files /dev/null and b/public/Intellifarms/favicon-48x48.png differ diff --git a/public/Intellifarms/favicon.ico b/public/Intellifarms/favicon.ico new file mode 100644 index 0000000..f3b5e0c Binary files /dev/null and b/public/Intellifarms/favicon.ico differ diff --git a/public/Intellifarms/manifest.json b/public/Intellifarms/manifest.json new file mode 100644 index 0000000..5288b8b --- /dev/null +++ b/public/Intellifarms/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Intellifarms", + "short_name": "Intellifarms", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/public/Intellifarms/mstile-150x150.png b/public/Intellifarms/mstile-150x150.png new file mode 100644 index 0000000..03b9ab0 Binary files /dev/null and b/public/Intellifarms/mstile-150x150.png differ diff --git a/public/Intellifarms/safari-pinned-tab.svg b/public/Intellifarms/safari-pinned-tab.svg new file mode 100644 index 0000000..6ea0a3f --- /dev/null +++ b/public/Intellifarms/safari-pinned-tab.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/whitelabels/Intellifarms/IFND-2023-Logo-White.png b/src/assets/whitelabels/Intellifarms/IFND-2023-Logo-White.png new file mode 100644 index 0000000..0afb3bf Binary files /dev/null and b/src/assets/whitelabels/Intellifarms/IFND-2023-Logo-White.png differ diff --git a/src/assets/whitelabels/Intellifarms/IFND-2023-Logo.png b/src/assets/whitelabels/Intellifarms/IFND-2023-Logo.png new file mode 100644 index 0000000..11ff1c8 Binary files /dev/null and b/src/assets/whitelabels/Intellifarms/IFND-2023-Logo.png differ diff --git a/src/bin/BinActions.tsx b/src/bin/BinActions.tsx index a425ea6..c9f7c8f 100644 --- a/src/bin/BinActions.tsx +++ b/src/bin/BinActions.tsx @@ -57,6 +57,8 @@ interface Props { components?: Map; setComponents?: React.Dispatch>>; updateBinStatus?: (componentKeys: string[], removed?: boolean) => void; + componentDevices?: Map + setComponentDevices?: React.Dispatch>>; } interface OpenState { @@ -77,7 +79,9 @@ export default function BinActions(props: Props) { refreshCallback, userID, components, + componentDevices, setComponents, + setComponentDevices, updateBinStatus } = props; const [anchorEl, setAnchorEl] = React.useState(null); @@ -245,7 +249,9 @@ export default function BinActions(props: Props) { open={openState.sensors} userID={userID} components={components} + componentDevices={componentDevices} setComponents={setComponents} + setComponentDevices={setComponentDevices} updateBinStatus={updateBinStatus} onClose={refresh => { if (refresh === true) { diff --git a/src/bin/BinComponents.tsx b/src/bin/BinComponents.tsx index 7864c46..0b81cba 100644 --- a/src/bin/BinComponents.tsx +++ b/src/bin/BinComponents.tsx @@ -71,7 +71,9 @@ const useStyles = makeStyles((theme: Theme) => { interface Props { components?: Map; + componentDevices?: Map; setComponents?: React.Dispatch>>; + setComponentDevices?: React.Dispatch>>; bin: string; binGrain: pond.Grain; updateBinStatus?: (componentKeys: string[], removed?: boolean) => void; @@ -84,7 +86,7 @@ interface Option { } export default function BinComponents(props: Props) { - const { components, bin, setComponents, updateBinStatus, binGrain } = props; + const { components, componentDevices, bin, setComponents, setComponentDevices, updateBinStatus, binGrain } = props; const [{as}] = useGlobalState(); const classes = useStyles(); const binAPI = useBinAPI(); @@ -208,20 +210,54 @@ export default function BinComponents(props: Props) { // } // }, [selectedDevice, componentAPI, deviceComponents, snackbar]); - const removeComponent = (component: string) => { - binAPI.removeComponent(bin, component, as).then(() => { + const removeComponent = (component: string, device?: number) => { + console.log(device) + binAPI.removeComponent(bin, component, device, as).then(() => { + if (components && setComponents) { if (components.delete(component)) { let newComponents = new Map(components); setComponents(newComponents); } } + if(componentDevices && setComponentDevices) { + if (componentDevices.delete(component)) { + let newComponentDevices = new Map(componentDevices); + setComponentDevices(newComponentDevices); + } + } snackbar.info("Component removed from bin"); }); setComponentToRemove(undefined); }; - const removeComponentConfirmation = () => { + const removeComponentConfirmation = (last?: boolean) => { + let lastComponent = false; + let devId: number | undefined; + if(last){ + lastComponent = last + }else{ + if (componentDevices && componentToRemove) { + const id = componentDevices.get(componentToRemove.key()); + + if (id !== undefined) { + devId = id + let count = 0; + + for (const val of componentDevices.values()) { + if (val === devId) { + count++; + + if (count > 1) { + break; // stop early once we know it's not the last + } + } + } + + lastComponent = count === 1; + } + } + } return ( setComponentToRemove(undefined)}> Remove {componentToRemove?.name()}? - - This will remove {componentToRemove?.name()} from this bin. If you don't have direct - access to this component or the device it is attached to, you will not be able to add it - back. - + {lastComponent ? + + This component {componentToRemove?.name()} is the last component from its device. You can choose to remove only the component itself or the device along with it. + Leaving the device may have unexpected results if the device will continue to be used and it is recommended to remove it if the device is being moved to a new bin. + + : + + This will remove {componentToRemove?.name()} from this bin. If you don't have direct + access to this component or the device it is attached to, you will not be able to add it + back. + + } + {lastComponent + ? + + + + : + } ); @@ -256,7 +310,7 @@ export default function BinComponents(props: Props) { Remove All Components? - This will remove All attached components from this bin. If you don't have direct access + This will remove All attached components and devices from this bin. If you don't have direct access to these components or the devices they are attached to, you will not be able to add them back. @@ -320,6 +374,12 @@ export default function BinComponents(props: Props) { setComponents(newComponents); } } + if(componentDevices && setComponentDevices) { + if (componentDevices.set(component.key(), device)) { + let newComponentDevices = new Map(componentDevices); + setComponentDevices(newComponentDevices); + } + } //if a grain cable was added to the bin update the components grain type to match the bin if (preferences.type === pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE) { let settings = component.settings; @@ -338,15 +398,40 @@ export default function BinComponents(props: Props) { } }); } else { - binAPI.removeComponent(bin, component.key(), as).then(resp => { - if (components && setComponents) { + let lastComponent = false; + if (componentDevices) { + console.log(componentDevices) + let count = 0; + for (const val of componentDevices.values()) { + if (val === device) { + count++; + + if (count > 1) { + break; // stop early once we know it's not the last + } + } + } + lastComponent = count === 1; + } + if(lastComponent){ + setComponentToRemove(component) + }else{ + binAPI.removeComponent(bin, component.key(), undefined, as).then(resp => { + if (components && setComponents) { if (components.delete(component.key())) { let newComponents = new Map(components); setComponents(newComponents); } } + if(componentDevices && setComponentDevices) { + if (componentDevices.delete(component.key())) { + let newComponentDevices = new Map(componentDevices); + setComponentDevices(newComponentDevices); + } + } snackbar.info("Component removed from bin"); - }); + }); + } } }; @@ -567,7 +652,10 @@ export default function BinComponents(props: Props) { {component.name()} - setComponentToRemove(component)}> + { + setComponentToRemove(component) + + }}> diff --git a/src/bin/BinSensors.tsx b/src/bin/BinSensors.tsx index f1c81ac..492091e 100644 --- a/src/bin/BinSensors.tsx +++ b/src/bin/BinSensors.tsx @@ -30,7 +30,9 @@ interface Props { coords?: { longitude: number; latitude: number }; binYards?: pond.BinYardSettings[]; components?: Map; + componentDevices?: Map setComponents?: React.Dispatch>>; + setComponentDevices?: React.Dispatch>>; updateBinStatus?: (componentKeys: string[], removed?: boolean) => void; } @@ -43,7 +45,9 @@ export default function BinSensors(props: Props) { mode, openedBinYard, components, + componentDevices, setComponents, + setComponentDevices, updateBinStatus } = props; const [initialized, setInitialized] = useState(false); @@ -93,7 +97,9 @@ export default function BinSensors(props: Props) { void; colourAboveZero?: ColourData colourBelowZero?: ColourData + minY?: string | number + maxY?: string | number } export default function SingleSetAreaChart(props: Props) { - const { data, maxRef, minRef, newXDomain, multiGraphZoom, yAxisLabel, colourAboveZero, colourBelowZero, tooltipLabel, tooltipUnit } = props; + const { data, maxRef, minRef, newXDomain, multiGraphZoom, yAxisLabel, colourAboveZero, colourBelowZero, tooltipLabel, tooltipUnit, minY, maxY } = props; const [xDomain, setXDomain] = useState(["dataMin", "dataMax"]); const [refLeft, setRefLeft] = useState(); const [refRight, setRefRight] = useState(); @@ -152,7 +154,8 @@ export default function SingleSetAreaChart(props: Props) { /> { if (path === "") { - if (isAdaptive) { + if (isAg || isIntel) { return "bins"; } if (isMiVent) { @@ -74,7 +75,7 @@ export default function BottomNavigator(props: Props) { } return path; }, - [isAdaptive, isMiVent] + [isAg, isMiVent, isIntel] ); const autoDetectRoute = useCallback(() => { @@ -105,10 +106,10 @@ export default function BottomNavigator(props: Props) { const authenticatedNavigation = () => { return ( handleRouteChange(newValue)}> - {isAdaptive && ( + {isAg || isIntel && ( } value="visualFarm" /> )} - {isAdaptive && ( + {isAg || isIntel && ( } value="bins" /> )} {isAdCon && ( @@ -136,7 +137,7 @@ export default function BottomNavigator(props: Props) { ) : isAdCon ? ( diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 6bab36c..815f30b 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -27,6 +27,7 @@ import { IsAdaptiveAgriculture, // hasTutorialPlaylist, IsAdCon, + IsIntellifarms, IsMiPCA, // isBXT, IsMiVent, @@ -165,14 +166,15 @@ export default function SideNavigator(props: Props) { const authenticatedSideMenu = () => { const isMiVent = IsMiVent(); - const isAg = IsAdaptiveAgriculture() + const isAg = IsAdaptiveAgriculture() + const isIntel = IsIntellifarms() const isStreamline = IsStreamline() const isOmni = IsOmniAir() const isMiPCA = IsMiPCA() const isAdCon = IsAdCon() return ( - {(isAg || isStreamline || user.hasFeature("admin")) && ( + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( )} - {(isAg || isStreamline || user.hasFeature("admin")) && ( + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( )} - {(isAg || isStreamline || user.hasFeature("admin")) && ( + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( } - {(isAg || isStreamline || user.hasFeature("admin")) && ( + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( } - {(isAg || isStreamline || user.hasFeature("admin")) && ( + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( )} - {(isAg || isStreamline || user.hasFeature("admin")) && ( + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && ( } - {(isAg || isStreamline || user.hasFeature("admin")) && + {(isAg || isIntel || isStreamline || user.hasFeature("admin")) && { let device = Device.create(dev); if (attachedDevIds.includes(device.id())) { - devs.push(Device.create(dev)); + devs.push(device); } }); setDevices(devs); @@ -676,6 +676,8 @@ export default function Bin(props: Props) { userID={user.id()} components={components} setComponents={setComponents} + componentDevices={componentDevices} + setComponentDevices={setComponentDevices} updateBinStatus={updateStatus} /> diff --git a/src/providers/pond/binAPI.tsx b/src/providers/pond/binAPI.tsx index def6699..e0d46f9 100644 --- a/src/providers/pond/binAPI.tsx +++ b/src/providers/pond/binAPI.tsx @@ -30,6 +30,7 @@ export interface IBinAPIContext { removeComponent: ( bin: string, component: string, + device?: number, otherTeam?: string ) => Promise>; removeAllComponents: (bin: string, otherTeam?: string) => Promise>; @@ -254,10 +255,11 @@ export default function BinProvider(props: PropsWithChildren) { }) }; - const removeComponent = (bin: string, component: string, otherTeam?: string) => { + const removeComponent = (bin: string, component: string, device?: number, otherTeam?: string) => { const view = otherTeam ? otherTeam : as let url = "/bins/" + bin + "/removeComponent/" + component; - if (view) url = url + "?as=" + view; + if (device) url = url + "?deviceToRemove=" + device + if (view) url = url + (device ? "&as=" : "?as=") + view; return new Promise>((resolve, reject)=>{ post(pondURL(url)).then(resp => { resp.data = pond.RemoveBinComponentResponse.fromObject(resp.data) diff --git a/src/services/whiteLabel.ts b/src/services/whiteLabel.ts index 29c4427..e53ef6a 100644 --- a/src/services/whiteLabel.ts +++ b/src/services/whiteLabel.ts @@ -12,6 +12,8 @@ import MiVentLightLogo from "../assets/whitelabels/MiVent/lightLogo.png"; // import OmniAirLogo from "../assets/whitelabels/OmniAir/OmniAirLogo.png"; import MiPCALogo from "../assets/whitelabels/MiPCA/MiPCALogo.png"; import StreamlineLogo from "../assets/whitelabels/Streamline/stream-logo.png" +import IntellifarmsLogo from "../assets/whitelabels/Intellifarms/IFND-2023-Logo.png" +import IntellifarmsLogoWhite from "../assets/whitelabels/Intellifarms/IFND-2023-Logo-White.png" // import { green, yellow } from "@mui/material/colors"; const protips: string[] = [ @@ -180,6 +182,32 @@ export function IsAdaptiveAgriculture(): boolean { ); } +const INTELLIFARMS_WHITE_LABEL: WhiteLabel = { + name: "Intellifarms", + primaryColour: "#9E1B32", + secondaryColour: "#4A4F55", + signatureColour: "#272727", + signatureAccentColour: "#fff", + auth0ClientId: import.meta.env.VITE_AUTH0_INTELLIFARMS_CLIENT_ID, + redirectOnLogout: true, + logoutRedirectTarget: "https://myintellifarms.com", + darkLogo: IntellifarmsLogo, + lightLogo: IntellifarmsLogoWhite, + transparentLogoBG: true, + blacklist: [], + docs: "Platform", + protips: protips, + homePage: "/bins" +}; + +export function IsIntellifarms(): boolean { + return ( + getName() === "Intellifarms" + // window.location.origin.includes("staging") || + // window.location.origin.includes("localhost") + ); +} + export function IsStreamline(): boolean { return ( getName() === "Streamline" @@ -324,7 +352,8 @@ const whitelabels = new Map([ ["adaptiveconstruction", ADAPTIVE_CONSTRUCTION_WHITE_LABEL], ["omniair", OMNIAIR_WHITE_LABEL], ["mipca", MIPCA_WHITE_LABEL], - ["mionetech", MIPCA_WHITE_LABEL] + ["mionetech", MIPCA_WHITE_LABEL], + ["intellifarms", INTELLIFARMS_WHITE_LABEL] ]); export function getWhitelabel(): WhiteLabel { @@ -337,7 +366,7 @@ export function getWhitelabel(): WhiteLabel { return BXT_WHITE_LABEL; } if (window.location.origin.includes("localhost")) { - return STREAMLINE_WHITE_LABEL; + return INTELLIFARMS_WHITE_LABEL; } if (window.location.origin.includes("staging") || import.meta.env.VITE_LOCAL_STAGING=='true') { return STAGING_WHITELABEL;