Merge branch 'integrations' into dev_environment

This commit is contained in:
csawatzky 2025-04-28 16:11:01 -06:00
commit 765ac72431
13 changed files with 1396 additions and 68 deletions

View file

@ -44,6 +44,18 @@ function App() {
let client_id = import.meta.env.VITE_AUTH0_CLIENT_ID;
// if (!client_id) client_id = import.meta.env.VITE_AUTH0_CLIENT_ID;
//check the url for a code before auth0 causes a login re-direct
// if (window.location.pathname !== "/callback") {
// //set the code into local storage
// let code = new URLSearchParams(window.location.search).get("code");
// sessionStorage.setItem("code", code || "")
// }
const skipCallbacks = [
"/johndeere",
"/cnhi"
]
return (
<AppThemeProvider>
<Auth0Provider
@ -53,6 +65,7 @@ function App() {
audience: or(audience, ""),
redirect_uri: window.location.origin + "/callback"
}}
skipRedirectCallback={skipCallbacks.includes(window.location.pathname)}
useRefreshTokens={true}
cacheLocation='localstorage'
>

View file

@ -0,0 +1,199 @@
import {
Avatar,
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid,
MenuItem,
TextField
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { teamScope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useSnackbar, useUserAPI, useCNHiProxyAPI } from "providers";
import { useEffect, useState } from "react";
//import { useLocation } from "react-router";
import TeamSearch from "teams/TeamSearch";
export default function CNHiAccess() {
const [openDialog, setOpenDialog] = useState(false);
const [teamKey, setTeamKey] = useState("");
const [teamUsers, setTeamUsers] = useState<User[]>([]);
const [primaryUser, setPrimaryUser] = useState("");
const [cnhiUsername, setCNHiUserName] = useState("");
const [cnhiCode, setCNHiCode] = useState<string | null>("");
//const [activeStep, setActiveStep] = useState<number>(0);
const userAPI = useUserAPI();
const cnhiAPI = useCNHiProxyAPI();
//const [dataOps, setDataOps] = useState<pond.DataOption[]>([]);
const { openSnack } = useSnackbar();
//const search = useLocation().search;
//const searchParams = new URLSearchParams(search);
//const [{ as }] = useGlobalState();
const client_id = import.meta.env.VITE_CNHI_CLIENT_ID;
const auth_url = import.meta.env.VITE_CNHI_AUTHORIZE_URL;
const redirect_url = import.meta.env.VITE_CNHI_REDIRECT_URI;
const scopes = import.meta.env.VITE_CNHI_SCOPES;
const connection = import.meta.env.VITE_CNHI_CONNECTION;
const audience = import.meta.env.VITE_CNHI_AUDIENCE;
const submitNewOrganization = () => {
if (cnhiCode) {
cnhiAPI
.addAccount(teamKey, primaryUser, cnhiCode, cnhiUsername)
.then(resp => {
openSnack("Added New Case New Holland Account Link");
})
.catch(err => {
openSnack("Failed to Add New Case New Holland Account Link");
});
setOpenDialog(false);
}
};
useEffect(() => {
if (teamKey !== "") {
userAPI.listObjectUsers(teamScope(teamKey)).then(resp => {
setTeamUsers(resp.data.users.map((u: pond.User) => User.any(u)));
});
}
}, [teamKey, userAPI]);
useEffect(() => {
let code = new URLSearchParams(window.location.search).get("code");
if (code) {
setCNHiCode(code);
setOpenDialog(true);
}
}, [window.location, cnhiCode]);
const validate = () => {
let invalid = false;
if (cnhiUsername === "" || teamKey === "" || primaryUser === "" || cnhiCode === "") {
invalid = true;
}
return invalid;
};
const general = () => {
return (
<Box>
<TextField
margin="dense"
label="Case New Holland Email"
fullWidth
variant="outlined"
value={cnhiUsername}
onChange={e => setCNHiUserName(e.target.value)}
/>
<TeamSearch label="Team" setTeamCallback={setTeamKey} />
<TextField
disabled={teamKey === ""}
margin="dense"
label="Primary User"
select
fullWidth
variant="outlined"
value={primaryUser}
onChange={e => setPrimaryUser(e.target.value)}>
{teamUsers.map(u => (
<MenuItem key={u.id()} value={u.id()}>
<Grid container direction="row" alignItems="center">
<Grid item>
<Avatar
alt={u.name()}
src={
u.settings.avatar && u.settings.avatar !== "" ? u.settings.avatar : undefined
}
/>
</Grid>
<Grid item>
<Box marginLeft={2}>{u.name()}</Box>
</Grid>
</Grid>
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
label="code"
disabled
fullWidth
variant="outlined"
value={cnhiCode}
onChange={e => setCNHiCode(e.target.value)}
/>
</Box>
);
};
const newOrgDialog = () => {
return (
<ResponsiveDialog
open={openDialog}
onClose={() => {
setOpenDialog(false);
}}>
<DialogTitle>Enter New Integration Link</DialogTitle>
<DialogContent>{general()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDialog(false);
}}>
Close
</Button>
<Button
disabled={validate()}
onClick={submitNewOrganization}
variant="contained"
color="primary">
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<Box style={{ padding: 10 }}>
<Button
variant="contained"
color="primary"
onClick={e => {
e.preventDefault();
window.open(
`${auth_url}?client_id=${client_id}&response_type=code&scope=${scopes}&redirect_uri=${redirect_url}&audience=${audience}&connection=${connection}`,
"_blank"
);
}}>
Link Case New Holland Account
</Button>
{newOrgDialog()}
{/* Admin testing buttons */}
{/*
<Button
onClick={() => {
setOpenDialog(true);
}}>
2. Choose team and user
</Button>
<Button
onClick={() => {
cnhiAPI
.listFields(100, 0, as)
.then(resp => {
console.log(resp);
})
.catch(err => {
console.log("errors found");
});
}}>
Field List test
</Button> */}
</Box>
);
}

View file

@ -0,0 +1,212 @@
import {
Avatar,
Box,
Button,
DialogActions,
DialogContent,
DialogTitle,
Grid2 as Grid,
MenuItem,
TextField
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { teamScope, User } from "models";
import { pond } from "protobuf-ts/pond";
import { useSnackbar, useUserAPI, useJohnDeereProxyAPI } from "providers";
import { useEffect, useState } from "react";
//import { useLocation } from "react-router";
import TeamSearch from "teams/TeamSearch";
//const steps = [{label: "General"},{label: "Data Options"}]
export default function JDAccess() {
const [openDialog, setOpenDialog] = useState(false);
const [teamKey, setTeamKey] = useState("");
const [teamUsers, setTeamUsers] = useState<User[]>([]);
const [primaryUser, setPrimaryUser] = useState("");
const [jdUserName, setJDUserName] = useState("");
const [jdCode, setJDCode] = useState<string | null>("");
//const [activeStep, setActiveStep] = useState<number>(0);
const userAPI = useUserAPI();
const johnDeereAPI = useJohnDeereProxyAPI();
//const [dataOps, setDataOps] = useState<pond.DataOption[]>([]);
const { openSnack } = useSnackbar();
//const search = useLocation().search;
//const searchParams = new URLSearchParams(search);
//const [{ as }] = useGlobalState();
//const MAPBOX_TOKEN = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const client_id = import.meta.env.VITE_JD_CLIENT_ID;
const auth_url = import.meta.env.VITE_JD_AUTHORIZE_URL;
const redirect_url = import.meta.env.VITE_JD_REDIRECT_URI;
//const redirect_url = "http://localhost:5173/johndeere";//local redirect for testing
const scopes = import.meta.env.VITE_JD_SCOPES;
const state = import.meta.env.VITE_JD_STATE;
const submitNewOrganization = () => {
//api call to add the new organization
if (jdCode) {
johnDeereAPI
.addAccount(teamKey, primaryUser, jdCode, jdUserName)
.then(resp => {
openSnack("Added New John Deere Account Link");
window.open(
`https://connections.deere.com/connections/${client_id}/select-organizations`
);
})
.catch(err => {
openSnack("Failed to Add New John Deere Account Link");
});
setOpenDialog(false);
}
};
useEffect(() => {
if (teamKey !== "") {
userAPI.listObjectUsers(teamScope(teamKey)).then(resp => {
setTeamUsers(resp.data.users.map((u: pond.User) => User.any(u)));
});
}
}, [teamKey, userAPI]);
useEffect(() => {
let code = new URLSearchParams(window.location.search).get("code");
if (code) {
setJDCode(code);
setOpenDialog(true);
}
}, [window.location, jdCode]);
const validate = () => {
let invalid = false;
if (jdUserName === "" || teamKey === "" || primaryUser === "" || jdCode === "") {
invalid = true;
}
return invalid;
};
const general = () => {
return (
<Box>
<TextField
margin="dense"
label="John Deere Username"
fullWidth
variant="outlined"
value={jdUserName}
onChange={e => setJDUserName(e.target.value)}
/>
<TeamSearch label="Team" setTeamCallback={setTeamKey} />
<TextField
disabled={teamKey === ""}
margin="dense"
label="Primary User"
select
fullWidth
variant="outlined"
value={primaryUser}
onChange={e => setPrimaryUser(e.target.value)}>
{teamUsers.map(u => (
<MenuItem key={u.id()} value={u.id()}>
<Grid container direction="row" alignItems="center">
<Grid>
<Avatar
alt={u.name()}
src={
u.settings.avatar && u.settings.avatar !== "" ? u.settings.avatar : undefined
}
/>
</Grid>
<Grid>
<Box marginLeft={2}>{u.name()}</Box>
</Grid>
</Grid>
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
label="code"
disabled
fullWidth
variant="outlined"
value={jdCode}
onChange={e => setJDCode(e.target.value)}
/>
</Box>
);
};
const newOrgDialog = () => {
return (
<ResponsiveDialog
open={openDialog}
onClose={() => {
setOpenDialog(false);
}}>
<DialogTitle>Enter New Integration Link</DialogTitle>
<DialogContent>{general()}</DialogContent>
<DialogActions>
<Button
onClick={() => {
setOpenDialog(false);
}}>
Close
</Button>
<Button
disabled={validate()}
onClick={submitNewOrganization}
variant="contained"
color="primary">
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
};
return (
<Box style={{ padding: 10 }}>
<Button
variant="contained"
color="primary"
// onClick={e => {
// e.preventDefault();
// (window.location.href =
// // TODO: Remember to change the redirect uri to the correct address before putting this up on staging/prod
// `${auth_url}?response_type=code&scope=${scopes}&client_id=${client_id}&redirect_uri=${redirect_url}&state=${state}`),
// "_blank"
// }
onClick={e => {
e.preventDefault();
window.open(
`${auth_url}?response_type=code&scope=${scopes}&client_id=${client_id}&redirect_uri=${redirect_url}&state=${state}`,
"_blank"
);
}}>
Link John Deere Account
</Button>
{newOrgDialog()}
{/* admin buttons for testing */}
{/* <Button
onClick={() => {
johnDeereAPI
.listFields(100, 0, as)
.then(resp => {
console.log(resp);
})
.catch(err => {
console.log("errors found");
});
}}>
Field List test
</Button>
<Button
onClick={() => {
setOpenDialog(true);
}}>
2. Choose team and user
</Button>*/}
</Box>
);
}

View file

@ -25,7 +25,9 @@ import {
useBinAPI,
useFieldMarkerAPI,
useDeviceAPI,
useGrainBagAPI
useGrainBagAPI,
useJohnDeereProxyAPI,
useCNHiProxyAPI
} from "providers";
import { pond } from "protobuf-ts/pond";
import { Bin as IBin, Field, FieldMarker, Device as DeviceModel } from "models";
@ -95,8 +97,8 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
const isMobile = useMobile();
const userId = user.id();
const fieldAPI = useFieldAPI();
//const johnDeereAPI = useJohnDeereProxyAPI();
//const cnhiAPI = useCNHiProxyAPI();
const johnDeereAPI = useJohnDeereProxyAPI();
const cnhiAPI = useCNHiProxyAPI();
const { openSnack } = useSnackbar();
const grainBagAPI = useGrainBagAPI();
const binYardAPI = useBinYardAPI();
@ -246,61 +248,61 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
/**
* used to load the fields managed by John Deere
*/
// const loadJDFields = useCallback(() => {
// johnDeereAPI
// .listFields(100, 0, as)
// .then(resp => {
// let fields: Map<string, Field> = new Map();
// let fieldEntries: GeocoderObject[] = [];
// if (resp.data.fields) {
// resp.data.fields.forEach(f => {
// let field = Field.any(f);
// fields.set(field.key(), field);
// fieldEntries.push({
// id: field.key(),
// place_name: field.name(),
// place_type: ["field"],
// center: [field.center().longitude, field.center().latitude]
// });
// });
// }
// setJDFields(fields);
// setJDFieldSearchEntries(fieldEntries);
// })
// .catch(err => {
// openSnack("Failed to load JD Field Mappings");
// });
// }, [johnDeereAPI, as, openSnack]);
const loadJDFields = useCallback(() => {
johnDeereAPI
.listFields(100, 0, as)
.then(resp => {
let fields: Map<string, Field> = new Map();
let fieldEntries: Result[] = [];
if (resp.data.fields) {
resp.data.fields.forEach(f => {
let field = Field.any(f);
fields.set(field.key(), field);
fieldEntries.push({
id: field.key(),
place_name: field.name(),
place_type: ["field"],
center: [field.center().longitude, field.center().latitude]
} as Result);
});
}
setJDFields(fields);
setJDFieldSearchEntries(fieldEntries);
})
.catch(err => {
openSnack("Failed to load JD Field Mappings");
});
}, [johnDeereAPI, as, openSnack]);
/**
* used to load the fields managed by John Deere
*/
// const loadCNHIFields = useCallback(() => {
// cnhiAPI
// .listFields(100, 0, as)
// .then(resp => {
// let fields: Map<string, Field> = new Map();
// let fieldEntries: GeocoderObject[] = [];
// if (resp.data.fields) {
// resp.data.fields.forEach(f => {
// let field = Field.any(f);
// fields.set(field.key(), field);
// fieldEntries.push({
// id: field.key(),
// place_name: field.name(),
// place_type: ["field"],
// center: [field.center().longitude, field.center().latitude]
// });
// });
// }
const loadCNHIFields = useCallback(() => {
cnhiAPI
.listFields(100, 0, as)
.then(resp => {
let fields: Map<string, Field> = new Map();
let fieldEntries: Result[] = [];
if (resp.data.fields) {
resp.data.fields.forEach(f => {
let field = Field.any(f);
fields.set(field.key(), field);
fieldEntries.push({
id: field.key(),
place_name: field.name(),
place_type: ["field"],
center: [field.center().longitude, field.center().latitude]
} as Result);
});
}
// setCNHIFields(fields);
// setCNHIFieldSearchEntries(fieldEntries);
// })
// .catch(err => {
// openSnack("Failed to load CNHI Field Mappings");
// });
// }, [cnhiAPI, as, openSnack]);
setCNHIFields(fields);
setCNHIFieldSearchEntries(fieldEntries);
})
.catch(err => {
openSnack("Failed to load CNHI Field Mappings");
});
}, [cnhiAPI, as, openSnack]);
const loadGrainBags = useCallback(() => {
grainBagAPI
@ -761,8 +763,8 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
if (!loading) {
setLoading(true);
loadFields();
//loadJDFields();
//loadCNHIFields();
loadJDFields();
loadCNHIFields();
loadBins();
loadYards();
loadDevices();
@ -776,8 +778,8 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
loadDevices,
loadFieldMarkers,
loadFields,
//loadJDFields,
//loadCNHIFields,
loadJDFields,
loadCNHIFields,
loadGrainBags,
loading
]);

View file

@ -42,6 +42,8 @@ const Firmware = lazy(() => import("pages/Firmware"));
const Nfc = lazy(() => import("pages/Nfc"));
const Contracts = lazy(() => import("pages/Contracts"));
const Contract = lazy(() => import("pages/Contract"));
const JohnDeere = lazy(() => import("pages/JohnDeere"));
const CNHi = lazy(() => import("pages/CNHi"));
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
@ -322,7 +324,14 @@ export default function Router() {
<Route path="contracts" element={<Contracts />} />
<Route path="contracts/:contractKey" element={<Contract />} />
{/* Map pages */}
{/* integration routes */}
{user.hasFeature("john-deere") &&
<Route path="johndeere" element={<JohnDeere />} />
}
{user.hasFeature("cnhi") &&
<Route path="cnhi" element={<CNHi />} />
}
{/* Map routes */}
<Route path="visualFarm" element={<FieldMap />} />
<Route path="aviationMap" element={<AviationMap />} />
<Route path="constructionMap" element={<ConstructionSiteMap />} />

View file

@ -45,6 +45,8 @@ import FieldListIcon from "products/AgIcons/FieldList";
import MarketplaceIcon from "products/CommonIcons/marketplaceIcon";
import DataDuckIcon from "products/Bindapt/DataDuckIcon";
import ContractsIcon from "products/CommonIcons/contractIcon";
import JohnDeereIcon from "products/CommonIcons/johnDeereIcon";
import CNHiIcon from "products/CommonIcons/cnhiIcon";
const drawerWidth = 230;
@ -442,6 +444,34 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Marketplace" />}
</ListItemButton>
</Tooltip>
{user.hasFeature("john-deere") &&
<Tooltip title="John Deere" placement="right">
<ListItemButton
id="tour-jd"
onClick={() => goTo("/johndeere")}
classes={getClasses("/johndeere")}
>
<ListItemIcon>
<JohnDeereIcon />
</ListItemIcon>
{open && <ListItemText primary="John Deere" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("cnhi") &&
<Tooltip title="Case New Holland" placement="right">
<ListItemButton
id="tour-cnhi"
onClick={() => goTo("/cnhi")}
classes={getClasses("/cnhi")}
>
<ListItemIcon>
<CNHiIcon />
</ListItemIcon>
{open && <ListItemText primary="Case New Holland" />}
</ListItemButton>
</Tooltip>
}
</List>
)
}

148
src/pages/CNHi.tsx Normal file
View file

@ -0,0 +1,148 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Checkbox,
FormControlLabel,
Grid2 as Grid,
MenuItem,
Select
} from "@mui/material";
import { ExpandMore } from "@mui/icons-material";
import CNHiAccess from "integrations/CNHi/CNHiAccess";
import { pond } from "protobuf-ts/pond";
import { useGlobalState, useCNHiProxyAPI } from "providers";
import React, { useEffect, useState } from "react";
import PageContainer from "./PageContainer";
export default function CNHi() {
const [currentOrg, setCurrentOrg] = useState("");
const cnhiAPI = useCNHiProxyAPI();
const [organizations, setOrganizations] = useState<Map<string, pond.JDAccount>>(new Map());
const [fieldAccordion, setFieldAccordion] = useState(false);
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
const [{ as }] = useGlobalState();
//load organizations for the user
useEffect(() => {
setCurrentOrg("");
cnhiAPI
.listAccounts(0, 0, as)
.then(resp => {
let tempOrgs: Map<string, pond.JDAccount> = new Map();
resp.data.accounts.forEach(org => {
let organization = pond.JDAccount.fromObject(org);
tempOrgs.set(organization.key, organization);
});
setOrganizations(tempOrgs);
})
.catch(err => {});
}, [cnhiAPI, as]);
useEffect(() => {
let organization = organizations.get(currentOrg);
if (organization) {
let currentOptions: pond.DataOption[] = organization.options ?? [];
setDataOptions(currentOptions);
}
}, [currentOrg, organizations]);
const updateOrgData = (checked: boolean, option: pond.DataOption) => {
let currentOps: pond.DataOption[] = dataOptions;
if (checked && !currentOps.includes(option)) {
currentOps.push(option);
} else if (!checked && currentOps.includes(option)) {
currentOps.splice(currentOps.indexOf(option), 1);
}
setDataOptions([...currentOps]);
};
const submit = () => {
cnhiAPI.updateAccount(currentOrg, dataOptions, as ?? undefined).then(resp => {
//update the organization in the map to have the correct dataOptions
let org = organizations.get(currentOrg);
if (org) {
org.options = dataOptions;
}
});
};
const fieldOptions = () => {
return (
<React.Fragment>
<Accordion
expanded={fieldAccordion}
onChange={(_, expanded) => {
setFieldAccordion(expanded);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>Field Data</AccordionSummary>
<AccordionDetails>
<Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
<Grid size={6}>
<FormControlLabel
label="Field Boundaries"
control={
<Checkbox
checked={dataOptions.includes(pond.DataOption.DATA_OPTION_FIELDS)}
onChange={(_, checked) => {
//setFields(!fields);
updateOrgData(checked, pond.DataOption.DATA_OPTION_FIELDS);
}}
/>
}
/>
</Grid>
<Grid size={6}>
View Field Boundaries from Case New Holland on your visual Farm and set up harvest
plans and track tasks
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
</React.Fragment>
);
};
return (
<PageContainer>
<CNHiAccess />
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center"
style={{ padding: 10 }}>
<Grid>
<Select
//style={{ maxWidth: 110 }}
title="John Deer Account"
displayEmpty
//disableUnderline={true}
value={currentOrg}
onChange={(event: any) => {
setCurrentOrg(event.target.value);
}}>
<MenuItem key={""} value={""}>
Select account to adjust accessable data
</MenuItem>
{Array.from(organizations.values()).map(org => {
return (
<MenuItem key={org.key} value={org.key}>
{org.username}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid>
<Button onClick={submit} variant="contained" color="primary">
Update
</Button>
</Grid>
</Grid>
<Box style={{ padding: 10 }}>{fieldOptions()}</Box>
</PageContainer>
);
}

453
src/pages/JohnDeere.tsx Normal file
View file

@ -0,0 +1,453 @@
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Checkbox,
FormControlLabel,
Grid2 as Grid,
MenuItem,
Select
} from "@mui/material";
import { ExpandMore } from "@mui/icons-material";
import JDAccess from "integrations/JohnDeere/JDAccess";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useJohnDeereProxyAPI } from "providers/pond/johnDeereProxyAPI";
import React, { useEffect, useState } from "react";
import PageContainer from "./PageContainer";
export default function JohnDeere() {
const jdAPI = useJohnDeereProxyAPI();
const [{ as }] = useGlobalState();
const [organizations, setOrganizations] = useState<Map<string, pond.JDAccount>>(new Map());
const [currentOrg, setCurrentOrg] = useState<string>("");
//booleans for what is selected for import with the jd organization
//const [fields, setFields] = useState(false);
const [dataOptions, setDataOptions] = useState<pond.DataOption[]>([]);
//accordions status
const [fieldAccordion, setFieldAccordion] = useState(false);
// const [equipmentAccordion, setEquipmentAccordion] = useState(false);
// const [additivesAccordion, setAdditivesAccordion] = useState(false);
// const [managementAccordion, setManagementAccordion] = useState(false);
//load organizations for the user
useEffect(() => {
setCurrentOrg("");
jdAPI
.listAccounts(0, 0, as)
.then(resp => {
let tempOrgs: Map<string, pond.JDAccount> = new Map();
resp.data.accounts.forEach(org => {
let organization = pond.JDAccount.fromObject(org);
tempOrgs.set(organization.key, organization);
});
setOrganizations(tempOrgs);
})
.catch(err => {});
}, [jdAPI, as]);
useEffect(() => {
let organization = organizations.get(currentOrg);
if (organization) {
let currentOptions: pond.DataOption[] = organization.options ?? [];
setDataOptions(currentOptions);
}
}, [currentOrg, organizations]);
const updateOrgData = (checked: boolean, option: pond.DataOption) => {
// let organization = organizations.get(currentOrg);
// if (organization) {
// if (checked && !organization.options.includes(option)) {
// organization.options.push(option);
// } else if (!checked && organization.options.includes(option)) {
// organization.options.splice(organization.options.indexOf(option), 1);
// }
// }
let currentOps: pond.DataOption[] = dataOptions;
if (checked && !currentOps.includes(option)) {
currentOps.push(option);
} else if (!checked && currentOps.includes(option)) {
currentOps.splice(currentOps.indexOf(option), 1);
}
setDataOptions([...currentOps]);
};
const submit = () => {
jdAPI.updateAccount(currentOrg, dataOptions, as ?? undefined).then(resp => {
//update the organization in the map to have the correct dataOptions
let org = organizations.get(currentOrg);
if (org) {
org.options = dataOptions;
}
});
};
const fieldOptions = () => {
return (
<React.Fragment>
<Accordion
expanded={fieldAccordion}
onChange={(_, expanded) => {
setFieldAccordion(expanded);
}}>
<AccordionSummary expandIcon={<ExpandMore />}>Field Data</AccordionSummary>
<AccordionDetails>
<Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
<Grid size={6}>
<FormControlLabel
label="Field Boundaries"
control={
<Checkbox
checked={dataOptions.includes(pond.DataOption.DATA_OPTION_FIELDS)}
onChange={(_, checked) => {
//setFields(!fields);
updateOrgData(checked, pond.DataOption.DATA_OPTION_FIELDS);
}}
/>
}
/>
</Grid>
<Grid size={6}>
View Field Boundaries from John Deere and set up harvest plans and track tasks
</Grid>
{/* not yet implemented in the backend */}
{/* <Grid item xs={6}>
<FormControlLabel
label="Field Flags"
control={
<Checkbox
checked={dataOptions.includes(pond.DataOption.DATA_OPTION_FLAGS)}
onChange={(_, checked) => {
updateOrgData(checked, pond.DataOption.DATA_OPTION_FLAGS);
}}
/>
}
/>
</Grid>
<Grid item xs={6}>
Plots your flags as markers on your Visual Farm
</Grid> */}
</Grid>
</AccordionDetails>
</Accordion>
</React.Fragment>
);
};
// const equipmentOptions = () => {
// return (
// <React.Fragment>
// <Accordion
// expanded={equipmentAccordion}
// onChange={(_, expanded) => {
// setEquipmentAccordion(expanded);
// }}>
// <AccordionSummary expandIcon={<ExpandMore />}>Equipment</AccordionSummary>
// <AccordionDetails>
// <Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
// <Grid item xs={6}>
// <FormControlLabel
// label="Machinery"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_MACHINES)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_MACHINES);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View information about your machinery from John Deere
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Implements"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_IMPLEMENTS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_IMPLEMENTS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// Implement Information
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Maintenance"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_MAINTENENCE)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_MAINTENENCE);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// Equipment Maitenance details
// </Grid>
// </Grid>
// </AccordionDetails>
// </Accordion>
// </React.Fragment>
// );
// };
// const additivesOptions = () => {
// return (
// <React.Fragment>
// <Accordion
// expanded={additivesAccordion}
// onChange={(_, expanded) => {
// setAdditivesAccordion(expanded);
// }}>
// <AccordionSummary expandIcon={<ExpandMore />}>Additives</AccordionSummary>
// <AccordionDetails>
// <Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
// <Grid item xs={6}>
// <FormControlLabel
// label="Chemicals"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_CHEMICALS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_CHEMICALS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View information about chemicals sprayed on fields
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Varieties"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_VARIETIES)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_VARIETIES);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// Not sure what these are, may be grain varieties in which case this should go into
// the field data most likely
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Fertilizers"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_FERTILIZER)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_FERTILIZER);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// Information about fetilizer used on fields
// </Grid>
// </Grid>
// </AccordionDetails>
// </Accordion>
// </React.Fragment>
// );
// };
// const managementOptions = () => {
// return (
// <React.Fragment>
// <Accordion
// expanded={managementAccordion}
// onChange={(_, expanded) => {
// setManagementAccordion(expanded);
// }}>
// <AccordionSummary expandIcon={<ExpandMore />}>Management</AccordionSummary>
// <AccordionDetails>
// <Grid container direction="row" spacing={2} alignItems="center" alignContent="center">
// <Grid item xs={6}>
// <FormControlLabel
// label="Staff"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_STAFF)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_STAFF);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View information about farm staff
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="files"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_FILES)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_FILES);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// Not sure what this pulls
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Tasks"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_TASKS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_TASKS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// Pull tasks into our task management system
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Notifications"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_NOTIFICATIONS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_NOTIFICATIONS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View Notifications and events
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Assets"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_ASSETS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_ASSETS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View Assets registered with john deere
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Operators"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_OPERATORS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_OPERATORS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View Operators
// </Grid>
// <Grid item xs={6}>
// <FormControlLabel
// label="Clients"
// control={
// <Checkbox
// checked={dataOptions.includes(pond.DataOption.DATA_OPTION_CLIENTS)}
// onChange={(_, checked) => {
// updateOrgData(checked, pond.DataOption.DATA_OPTION_CLIENTS);
// }}
// />
// }
// />
// </Grid>
// <Grid item xs={6}>
// View Client information
// </Grid>
// </Grid>
// </AccordionDetails>
// </Accordion>
// </React.Fragment>
// );
// };
return (
<PageContainer>
<JDAccess />
<Grid
container
direction="row"
justifyContent="space-between"
alignContent="center"
alignItems="center"
style={{ padding: 10 }}>
<Grid>
<Select
//style={{ maxWidth: 110 }}
title="John Deer Account"
displayEmpty
value={currentOrg}
onChange={(event: any) => {
setCurrentOrg(event.target.value);
}}>
<MenuItem key={""} value={""}>
Select account to adjust accessable data
</MenuItem>
{Array.from(organizations.values()).map(org => {
return (
<MenuItem key={org.key} value={org.key}>
{org.username}
</MenuItem>
);
})}
</Select>
</Grid>
<Grid>
<Button onClick={submit} variant="contained" color="primary">
Update
</Button>
</Grid>
</Grid>
<Box style={{ padding: 10 }}>
{/* Field data accordion */}
{fieldOptions()}
{/* these three are not yet implemented TODO: reveal these options once implemented in the backend */}
{/*
{equipmentOptions()}
{additivesOptions()}
{managementOptions()}
*/}
</Box>
</PageContainer>
);
}

View file

@ -127,7 +127,8 @@ export default function Users() {
"grain-composition",
"developer",
"marketplace",
"installer"
"installer",
"cnhi"
].sort();
const [rows, setRows] = useState<User[]>([])

View file

@ -43,7 +43,9 @@ export {
useGrainBagAPI,
useTransactionAPI,
useFileControllerAPI,
useContractAPI //TODO: update api with resolve, reject
useContractAPI, //TODO: update api with resolve, reject
useJohnDeereProxyAPI, //TODO: update api with resolve, reject
useCNHiProxyAPI //TODO: update api with resolve, reject
} from "./pond/pond";
// export { SecurityContext, useSecurity } from "./security";
export { SnackbarContext, useSnackbar } from "./Snackbar";

View file

@ -0,0 +1,124 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { createContext, PropsWithChildren, useContext } from "react";
//import { or } from "utils";
import { pondURL } from "./pond";
export interface ICNHiProxyAPIContext {
//add new organization
addAccount: (
teamKey: string,
userID: string,
cnhiCode: string,
cnhiUsername: string
) => Promise<AxiosResponse<pond.AddCNHiAccountResponse>>;
//list organizations
listAccounts: (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListCNHiAccountsResponse>>;
updateAccount: (
key: string,
options: pond.DataOption[],
as?: string
) => Promise<AxiosResponse<pond.UpdateCNHiAccountResponse>>;
listFields: (
limit: number,
offset: number,
// order?: "asc" | "desc",
// orderBy?: string,
// search?: string,
as?: string,
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListCNHiFieldsResponse>>;
}
export const CNHiProxyAPIContext = createContext<ICNHiProxyAPIContext>({} as ICNHiProxyAPIContext);
interface Props {}
export default function CNHiProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { post, get, put } = useHTTP();
const addAccount = (teamKey: string, userID: string, cnhiCode: string, cnhiUsername: string) => {
return post<pond.AddCNHiAccountResponse>(
pondURL(
"/cnhiAccounts?team=" +
teamKey +
"&user=" +
userID +
"&code=" +
cnhiCode +
"&cnhiUsername=" +
cnhiUsername
)
);
};
const listAccounts = (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => {
return get<pond.ListCNHiAccountsResponse>(
pondURL(
"/cnhiAccounts?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const listFields = (
limit: number,
offset: number,
// order?: "asc" | "desc",
// orderBy?: string,
// search?: string,
as?: string,
asRoot?: boolean
) => {
return get<pond.ListFieldsResponse>(
pondURL(
"/cnhiFields" +
"?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "")
)
);
};
const updateAccount = (key: string, options: pond.DataOption[], as?: string) => {
return put<pond.UpdateCNHiAccountResponse>(
pondURL("/cnhiAccounts/" + key + "?options=" + options.toString() + (as ? "&as=" + as : ""))
);
};
return (
<CNHiProxyAPIContext.Provider
value={{
addAccount,
listAccounts,
updateAccount,
listFields
}}>
{children}
</CNHiProxyAPIContext.Provider>
);
}
export const useCNHiProxyAPI = () => useContext(CNHiProxyAPIContext);

View file

@ -0,0 +1,127 @@
import { AxiosResponse } from "axios";
import { useHTTP } from "hooks";
import { pond } from "protobuf-ts/pond";
import { createContext, PropsWithChildren, useContext } from "react";
//import { or } from "utils";
import { pondURL } from "./pond";
export interface IJohnDeereProxyAPIContext {
//add new organization
addAccount: (
teamKey: string,
userID: string,
jdCode: string,
jdUserName: string
) => Promise<AxiosResponse<pond.AddJDAccountResponse>>;
//list organizations
listAccounts: (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => Promise<AxiosResponse<pond.ListJDAccountsResponse>>;
updateAccount: (
key: string,
options: pond.DataOption[],
as?: string
) => Promise<AxiosResponse<pond.UpdateJDAccountResponse>>;
//update organizations
listFields: (
limit: number,
offset: number,
// order?: "asc" | "desc",
// orderBy?: string,
// search?: string,
as?: string,
asRoot?: boolean
) => Promise<AxiosResponse<pond.ListJDFieldsResponse>>;
}
export const JohnDeereProxyAPIContext = createContext<IJohnDeereProxyAPIContext>(
{} as IJohnDeereProxyAPIContext
);
interface Props {}
export default function JohnDeereProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { post, get, put } = useHTTP();
const addAccount = (teamKey: string, userID: string, jdCode: string, jdUserName: string) => {
return post<pond.AddJDAccountResponse>(
pondURL(
"/jdAccounts?team=" +
teamKey +
"&user=" +
userID +
"&code=" +
jdCode +
"&jdUserName=" +
jdUserName
)
);
};
const listAccounts = (
limit: number,
offset: number,
as?: string,
keys?: string[],
types?: string[]
) => {
return get<pond.ListJDAccountsResponse>(
pondURL(
"/jdAccounts?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(keys ? "&keys=" + keys.join(",") : "") +
(types ? "&types=" + types.join(",") : "")
)
);
};
const updateAccount = (key: string, options: pond.DataOption[], as?: string) => {
return put<pond.UpdateJDAccountResponse>(
pondURL("/jdAccounts/" + key + "?options=" + options.toString() + (as ? "&as=" + as : ""))
);
};
const listFields = (
limit: number,
offset: number,
// order?: "asc" | "desc",
// orderBy?: string,
// search?: string,
as?: string,
asRoot?: boolean
) => {
return get<pond.ListFieldsResponse>(
pondURL(
"/jdFields" +
"?limit=" +
limit +
"&offset=" +
offset +
(as ? "&as=" + as : "") +
(asRoot ? "&asRoot=" + asRoot.toString() : "")
)
);
};
return (
<JohnDeereProxyAPIContext.Provider
value={{
addAccount,
listAccounts,
updateAccount,
listFields
}}>
{children}
</JohnDeereProxyAPIContext.Provider>
);
}
export const useJohnDeereProxyAPI = () => useContext(JohnDeereProxyAPIContext);

View file

@ -34,6 +34,8 @@ import DataDogProvider, { useDataDogProxyAPI } from "./datadogProxyAPI";
import ContractProvider, { useContractAPI } from "./contractAPI";
import UsageProvider, { useUsageAPI } from "./usageAPI";
import KeyManagerProvider, { useKeyManagerAPI } from "./keyManagerAPI";
import JohnDeereProvider, { useJohnDeereProxyAPI } from "./johnDeereProxyAPI";
import CNHiProvider, { useCNHiProxyAPI } from "./cnhiProxyAPI";
// import NoteProvider from "providers/noteAPI";
export const pondURL = (partial: string, demo: boolean = false): string => {
@ -82,11 +84,15 @@ export default function PondProvider(props: PropsWithChildren<any>) {
<TaskProvider>
<DataDogProvider>
<ContractProvider>
<JohnDeereProvider>
<CNHiProvider>
<UsageProvider>
<KeyManagerProvider>
{children}
</KeyManagerProvider>
</UsageProvider>
</CNHiProvider>
</JohnDeereProvider>
</ContractProvider>
</DataDogProvider>
</TaskProvider>
@ -155,5 +161,7 @@ export {
useDataDogProxyAPI,
useContractAPI,
useUsageAPI,
useKeyManagerAPI
useKeyManagerAPI,
useJohnDeereProxyAPI,
useCNHiProxyAPI
};