diff --git a/src/bin/BinYards.tsx b/src/bin/BinYards.tsx index 092c542..0d371f0 100644 --- a/src/bin/BinYards.tsx +++ b/src/bin/BinYards.tsx @@ -50,6 +50,7 @@ import ObjectTeams from "teams/ObjectTeams"; import { GrainBag } from "models/GrainBag"; import { makeStyles } from "@mui/styles"; import DraggableTabs from "common/DraggableTabs"; +import CancelSubmit from "common/CancelSubmit"; const parentTab = { "&": { @@ -209,7 +210,8 @@ export default function BinYard(props: Props) { const [leaving, setLeaving] = useState(false); const [yardPermissions, setYardPermissions] = useState>({}); const [searchSelected, setSearchSelected] = useState(false); - const [{ user, as }] = useGlobalState(); + const [{ user }] = useGlobalState(); + const [addingYard, setAddingYard] = useState(false) useEffect(() => { if (props.yards && props.yardPerms) { @@ -231,22 +233,36 @@ export default function BinYard(props: Props) { }; const submitYard = () => { + if (addingYard) return + setAddingYard(true) let newBinYard = pond.BinYardSettings.create(); newBinYard.name = addYardName; newBinYard.description = addYardDescription; newBinYard.owner = user.id(); binYardAPI .addBinYard(newBinYard) - .then(_resp => { + .then(resp => { // loadYards(); let newYards = [...binYards] newYards.push(newBinYard) setBinYards(newYards) setShowAddYard(false); + newBinYard.key = resp.data.yard + + let newYardPermissions: Dictionary = { ...yardPermissions }; + newYardPermissions[newBinYard.key] = [ + pond.Permission.PERMISSION_READ, + pond.Permission.PERMISSION_SHARE, + pond.Permission.PERMISSION_USERS, + pond.Permission.PERMISSION_WRITE + ]; + setYardPermissions(newYardPermissions) openSnackbar("success", "Bin Yard created :)"); }) .catch(_err => { error("Could not add bin yard"); + }).finally(() => { + setAddingYard(false) }); }; @@ -305,16 +321,11 @@ export default function BinYard(props: Props) { Create New Bin Yard {yardForm()} - - + setShowAddYard(false)} + onSubmit={submitYard} + /> ); diff --git a/src/common/DraggableTabs.tsx b/src/common/DraggableTabs.tsx index a2de42e..0021a18 100644 --- a/src/common/DraggableTabs.tsx +++ b/src/common/DraggableTabs.tsx @@ -32,16 +32,52 @@ export default function DraggableTabs(props: DraggableTabsProps) { return storedValue !== null ? JSON.parse(storedValue) : []; }); + const sanitize = (order: any[]) => { + let newOrder = [...order] + // if there's something less than 0, increase everything by one + while (newOrder.some(num => num < 0)) { + // Increase all numbers by 1 + newOrder = newOrder.map(num => num + 1); + } + + // find a skipped index and flatten + while (true) { + // Find min and max values + const min = Math.min(...newOrder); + const max = Math.max(...newOrder); + + // Create a set of all numbers that should be present + const shouldHave = new Set(); + for (let i = min; i <= max; i++) { + shouldHave.add(i); + } + + // Create a set of actual numbers + const actual = new Set(newOrder); + + // Find first missing number + let missing = null; + for (let num of shouldHave) { + if (!actual.has(num)) { + missing = num; + break; + } + } + + // If no numbers are missing, we're done + if (missing === null || missing === undefined) break; + + // Reduce all numbers greater than the missing number by 1 + newOrder = newOrder.map(num => num > missing ? num - 1 : num); + } + return newOrder + } + // save tab order to cache when it changes useEffect(() => { - console.log(tabOrder) - localStorage.setItem(cacheKey, JSON.stringify(tabOrder)); + localStorage.setItem(cacheKey, JSON.stringify(sanitize(tabOrder))); }, [tabOrder]); - // useEffect(() => { - // console.log(initialChildren.length) - // }, [initialChildren]) - const getKey = (node: ReactNode): string => { return (React.isValidElement(node) && 'key' in node && typeof node.key === 'string') ? node.key : ""; } @@ -66,7 +102,7 @@ export default function DraggableTabs(props: DraggableTabsProps) { // Find the missing key const removedIndex = oldKeys.findIndex(key => !newKeys.includes(key)); - setTabOrder(removeAndReduceFirst(tabOrder, removedIndex)) + setTabOrder(removeAndReduceFirst(sanitize(tabOrder), removedIndex)) } else if (tabOrder.length - initialChildren.length === -1) { let newTabOrder = [...tabOrder] diff --git a/src/common/ImgIcon.tsx b/src/common/ImgIcon.tsx index 87cc59b..55d1a13 100644 --- a/src/common/ImgIcon.tsx +++ b/src/common/ImgIcon.tsx @@ -1,7 +1,7 @@ import { Icon, Theme } from "@mui/material"; import { makeStyles } from "@mui/styles"; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles((_theme: Theme) => ({ icon: { display: "flex", // Ensure flexbox for centering justifyContent: "center", // Center horizontally @@ -14,7 +14,6 @@ const useStyles = makeStyles((theme: Theme) => ({ maxWidth: "100%", // Fit within container width width: "auto", // Maintain aspect ratio height: "auto", // Maintain aspect ratio - color: "black" }, })); diff --git a/src/component/ComponentOrder.tsx b/src/component/ComponentOrder.tsx index 97947bd..2ca19cf 100644 --- a/src/component/ComponentOrder.tsx +++ b/src/component/ComponentOrder.tsx @@ -74,7 +74,7 @@ interface ListProps { close: (refresh: boolean) => void; device: Device; components: Component[]; - devicePreferences: pond.UserPreferences; + devicePreferences: pond.DevicePreferences; } const filteredComponents = (components: Component[]) => { diff --git a/src/device/ArcGISDeviceData.tsx b/src/device/ArcGISDeviceData.tsx new file mode 100644 index 0000000..d049740 --- /dev/null +++ b/src/device/ArcGISDeviceData.tsx @@ -0,0 +1,157 @@ +import { + Button, + Checkbox, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControlLabel, + InputAdornment, + TextField +} from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { GetDefaultDateRange } from "common/time/DateRange"; +import DateSelect from "common/time/DateSelect"; +import { useDeviceAPI } from "hooks"; +import { Component, Device } from "models"; +import { Moment } from "moment"; +import React, { useState } from "react"; +import { downloadJSON } from "utils"; + +interface Props { + open: boolean; + close: (refresh: boolean) => void; + device: Device; + components: Component[]; +} + +export default function ArcGISDeviceData(props: Props) { + const { open, close, device, components } = props; + const [startDate, setStartDate] = useState(GetDefaultDateRange().start); + const [endDate, setEndDate] = useState(GetDefaultDateRange().end); + const [includedComponents, setIncludedComponents] = useState([]); + const [filename, setFilename] = useState(GetDefaultDateRange().start.toString()); + const [limit, setLimit] = useState(0); + const deviceAPI = useDeviceAPI(); + const [useFlat, setUseFlat] = useState(false); + const handleClose = () => { + setUseFlat(false); + close(false); + }; + + const updateDateRange = (start: Moment, end: Moment) => { + setStartDate(start); + setEndDate(end); + }; + + const includeComponent = (checked: boolean, comp: Component) => { + let inComps = includedComponents; + if (checked) { + inComps.push(comp); + } else if (!checked && inComps.includes(comp)) { + inComps.splice(inComps.indexOf(comp), 1); + } + setIncludedComponents(inComps); + }; + + const isFormValid = () => { + let valid = false; + if (filename.length > 0) { + valid = true; + } + return valid; + }; + + const componentSelector = () => { + return components.map(comp => ( + includeComponent(e.target.checked, comp)} color="primary" /> + } + /> + )); + }; + + const getMeasurements = () => { + if (useFlat) { + deviceAPI.listSimpleJSON(device.id()).then(resp => { + downloadJSON(resp.data, "Device" + device.id() + ".json"); + }); + } else { + deviceAPI + .listJSONMeasurements( + device.id(), + includedComponents, + startDate, + endDate, + limit, + 0, + "asc", + "timestamp" + ) + .then(resp => { + downloadJSON(resp.data, filename + ".json"); + }); + } + }; + + return ( + + Export JSON Data for Device + + setUseFlat(e.target.checked)} + color="primary" + /> + } + /> + + {!useFlat && ( + + setFilename(e.target.value)} + InputProps={{ + endAdornment: .json + }} + /> + + setLimit(+e.target.value)} + /> + {componentSelector()} + + )} + + + + + + + ); +} diff --git a/src/device/Connection.tsx b/src/device/Connection.tsx new file mode 100644 index 0000000..a65c5b1 --- /dev/null +++ b/src/device/Connection.tsx @@ -0,0 +1,148 @@ +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + InputAdornment, + TextField, + Theme, + Typography +} from "@mui/material"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { useDeviceAPI, useSnackbar } from "hooks"; +import { getContextKeys, getContextTypes } from "pbHelpers/Context"; +import { useState } from "react"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => { + return ({ + highlight: { + color: theme.palette.secondary.dark + } + }) +}); + +interface Props { + deviceID: number; + deviceName: string; + open: boolean; + close: (refresh: boolean) => void; +} + +export default function Connection(props: Props) { + const classes = useStyles(); + const deviceAPI = useDeviceAPI(); + const { success, error } = useSnackbar(); + const { deviceID, deviceName, open, close } = props; + const [gateway, setGateway] = useState(""); + const [password, setPassword] = useState(""); + const [passwordVisible, setPasswordVisible] = useState(false); + + const isGatewayValid = (): boolean => { + if (!gateway) { + return true; + } + return gateway.length <= 32 && gateway.length > 0; + }; + + const isPasswordValid = (): boolean => { + if (!password) { + return true; + } + + return password.length <= 32; + }; + + const isConnectionValid = (): boolean => { + return isGatewayValid() && isPasswordValid(); + }; + + const handleClose = () => { + close(false); + setGateway(""); + setPassword(""); + setPasswordVisible(false); + }; + + const handleSubmit = () => { + if (isConnectionValid()) { + let keys = getContextKeys(); + let types = getContextTypes(); + deviceAPI + .setWifi(deviceID, gateway, password, keys, types) + .then(() => success("Connection settings sent to " + deviceName)) + .catch(() => error("Failed to send connection settings to " + deviceName)) + .finally(() => handleClose()); + } + }; + + return ( + handleClose()} + aria-labelledby="device-connection"> + + Connection + + {deviceName} + + + + + By default your Wi-Fi enabled device will try to connect to the gateway{" "} + device with the password{" "} + configure. Your device must be connected to the + internet to change its Wi-Fi credentials. + + setGateway(event.target.value)} + error={!isGatewayValid()} + helperText={isGatewayValid() ? "" : "Cannot be more than 32 characters"} + margin="normal" + fullWidth + variant="outlined" + /> + setPassword(event.target.value)} + error={!isPasswordValid()} + helperText={isPasswordValid() ? "" : "Cannot be more than 32 characters"} + margin="normal" + fullWidth + variant="outlined" + InputProps={{ + endAdornment: ( + + setPasswordVisible(!passwordVisible)}> + {passwordVisible ? : } + + + ) + }} + /> + + + + + + + ); +} diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index 4e969df..1ae2344 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -34,15 +34,15 @@ import NotificationButton from "common/NotificationButton"; // import ComponentSettings from "component/ComponentSettings"; // import DeviceSettings from "device/DeviceSettings"; // import LoadDeviceProfile from "device/LoadDeviceProfile"; -// import { PauseData } from "device/PauseData"; -// import { ResumeData } from "device/ResumeData"; +import { PauseData } from "device/PauseData"; +import { ResumeData } from "device/ResumeData"; // import SaveDeviceProfile from "device/SaveDeviceProfile"; import SyncDevice from "device/SyncDevice"; // import UpgradeDevice from "device/UpgradeDevice"; // import InteractionSettings from "interactions/InteractionSettings"; import { cloneDeep } from "lodash"; // import { Component, Device, deviceScope, Interaction } from "models"; -import { Component, Device, deviceScope } from "models"; +import { Component, Device, deviceScope, Interaction } from "models"; // import { MatchParams } from "navigation/Routes"; // import { isShareableLink } from "pbHelpers/Device"; import { pond } from "protobuf-ts/pond"; @@ -65,6 +65,11 @@ import DeviceSettings from "device/DeviceSettings"; import ObjectTeams from "teams/ObjectTeams"; import ComponentSettings from "component/ComponentSettings"; import LoadDeviceProfile from "./LoadDeviceProfile"; +import InteractionSettings from "interactions/InteractionSettings"; +import SaveDeviceProfile from "./SaveDeviceProfile"; +import Connection from "./Connection"; +import ComponentOrder from "component/ComponentOrder"; +import ArcGISDeviceData from "./ArcGISDeviceData"; const useStyles = makeStyles((_theme: Theme) => { // const isMobile = useMobile() @@ -97,7 +102,7 @@ interface Props { device: Device; isPaused: boolean; components: Component[]; - // interactions: Interaction[]; + interactions: Interaction[]; refreshCallback: () => void; availablePositions: DeviceAvailabilityMap; availableOffsets: OffsetAvailabilityMap; @@ -130,7 +135,7 @@ export default function DeviceActions(props: Props) { device, isPaused, components, - // interactions, + interactions, refreshCallback, availablePositions, availableOffsets, @@ -409,7 +414,7 @@ export default function DeviceActions(props: Props) { isDialogOpen={or(isShareObjectDialogOpen, false)} closeDialogCallback={() => closeDialog("isShareObjectDialogOpen")} /> - {/* closeDialog("isInteractionSettingsOpen")} refreshCallback={refreshCallback} canEdit={canWrite} - /> */} + /> closeDialog("isRemoveSelfDialogOpen")} /> - {/* closeDialog("isSaveDeviceProfileOpen")} device={device} @@ -448,14 +453,14 @@ export default function DeviceActions(props: Props) { interactions={interactions} tagIds={device.status.tagKeys} refreshCallback={refreshCallback} - /> */} + /> closeDialog("isLoadDeviceProfileOpen")} device={device} refreshCallback={refreshCallback} /> - {/* */} + /> ); }; diff --git a/src/device/PauseData.tsx b/src/device/PauseData.tsx new file mode 100644 index 0000000..503f61f --- /dev/null +++ b/src/device/PauseData.tsx @@ -0,0 +1,71 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Divider, + Theme +} from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { useDeviceAPI, useSnackbar } from "hooks"; + +const useStyles = makeStyles((theme: Theme) => { + return ({ + dialogContent: { + margin: theme.spacing(2) + } + }) +}); + +interface Props { + id: number; + isOpen: boolean; + close: (refresh: boolean) => void; +} + +export function PauseData(props: Props) { + const { id, isOpen, close } = props; + const classes = useStyles(); + const { success, error } = useSnackbar(); + const deviceAPI = useDeviceAPI(); + + const pause = () => { + deviceAPI + .pause(id) + .then(() => success("Data will be paused shortly")) + .catch(() => error("Something went wrong")) + .then(() => close(true)); + }; + + return ( + close(false)} + aria-labelledby="pause-data-dialog"> + Pause Data + + + + Pausing data will disconnect your device from the cellular network. No new status, + measurements, or configuration will be sent between the device and the cloud until data is + resumed. Your device will continue to operate offline but support will be limited. +
+
+ You will continue to be charged at a reduced rate to reserve your SIM card. Resuming data + after it has been paused will result in a reactivation charge. +
+
+ + + + +
+ ); +} diff --git a/src/device/ResumeData.tsx b/src/device/ResumeData.tsx new file mode 100644 index 0000000..9e07100 --- /dev/null +++ b/src/device/ResumeData.tsx @@ -0,0 +1,66 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Divider, + Theme +} from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { useDeviceAPI, useSnackbar } from "hooks"; + +const useStyles = makeStyles((theme: Theme) => { + return ({ + dialogContent: { + margin: theme.spacing(2) + } + }) +}); + +interface Props { + id: number; + isOpen: boolean; + close: (refresh: boolean) => void; +} + +export function ResumeData(props: Props) { + const { id, isOpen, close } = props; + const classes = useStyles(); + const { success, error } = useSnackbar(); + const deviceAPI = useDeviceAPI(); + + const resume = () => { + deviceAPI + .resume(id) + .then(() => success("Data will resume shortly")) + .catch(() => error("Something went wrong")) + .then(() => close(true)); + }; + + return ( + close(false)} + aria-labelledby="resume-data-dialog"> + Resume Data + + + + Resuming data will reconnect your device to the cellular network. It may take up to an + hour before communication resumes. Resuming data will result in a reactivation charge. + + + + + + + + ); +} diff --git a/src/device/SaveDeviceProfile.tsx b/src/device/SaveDeviceProfile.tsx new file mode 100644 index 0000000..8e9a50e --- /dev/null +++ b/src/device/SaveDeviceProfile.tsx @@ -0,0 +1,297 @@ +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + Grid2 as Grid, + MenuItem, + TextField, + Theme, + Typography +} from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import DeleteButton from "common/DeleteButton"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import SearchSelect, { Option } from "common/SearchSelect"; +import { usePreferenceAPI, usePrevious, useSnackbar } from "hooks"; +import { cloneDeep } from "lodash"; +import { Backpack, Component, Device, Interaction } from "models"; +import { backpackOptions } from "pbHelpers/Backpack"; +import { DeviceComponentKey } from "pbHelpers/Component"; +import { ListDeviceProductDescribers } from "products/DeviceProduct"; +import { pond } from "protobuf-ts/pond"; +import { useBackpackAPI } from "providers/pond/backpackAPI"; +import React, { useCallback, useEffect, useState } from "react"; + +const useStyles = makeStyles((theme: Theme) => { + return ({ + contentSpacing: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1) + } + }) +}); + +interface Props { + isDialogOpen: boolean; + closeDialogCallback: Function; + refreshCallback: Function; + device: Device; + components: Component[]; + interactions: Interaction[]; + tagIds: string[]; +} + +export default function SaveDeviceProfile(props: Props) { + const backpackAPI = useBackpackAPI(); + const preferenceAPI = usePreferenceAPI(); + const classes = useStyles(); + const { + isDialogOpen, + closeDialogCallback, + refreshCallback, + device, + components, + interactions, + tagIds + } = props; + const { success, error } = useSnackbar(); + const prevOpen = usePrevious(isDialogOpen); + const [backpacks, setBackpacks] = useState([]); + const [backpackName, setBackpackName] = useState(); + const [loading, setLoading] = useState(false); + const [isNew, setIsNew] = useState(false); + const [targetBackpackID, setTargetBackpackID] = useState(0); + const [product, setProduct] = useState(0); + + const defaultState = () => { + setBackpacks([]); + setBackpackName(""); + setIsNew(false); + setTargetBackpackID(0); + }; + + const close = () => { + closeDialogCallback(); + defaultState(); + }; + + const loadBackpacks = useCallback(() => { + const { listBackpacks } = backpackAPI; + setLoading(true); + listBackpacks() + .then((response: any) => { + const rawBackpacks = response.data.backpacks; + const backpacks: Backpack[] = []; + + if (rawBackpacks && rawBackpacks.length > 0) { + rawBackpacks.forEach((b: any) => { + backpacks.push(Backpack.create(pond.Backpack.fromObject(b))); + }); + } + + setBackpacks(backpacks.sort((b1, b2) => (b1.name() > b2.name() ? 1 : -1))); + }) + .catch((err: any) => { + setBackpacks([]); + error(err ? err : "Error occured while loading device profiles"); + }) + .finally(() => setLoading(false)); + }, [backpackAPI, error]); + + useEffect(() => { + if (isDialogOpen === true && isDialogOpen !== prevOpen) { + loadBackpacks(); + } + }, [isDialogOpen, prevOpen, loadBackpacks]); + + const assembleBackpack = (): Promise => { + const { getPreferences } = preferenceAPI; + return new Promise((resolve, reject) => { + let getDevicePreference = getPreferences("device", [device.id().toString()]); + let getComponentPreferences = getPreferences( + "component", + components.map(c => DeviceComponentKey(device, c)) + ); + let promises = [getDevicePreference, getComponentPreferences]; + Promise.all(promises) + .then(responses => { + let rDevicePreferences = responses[0]; + let rComponentPreferences = responses[1]; + let componentPreferences = {} as { + [k: string]: pond.UserPreferences; + }; + rComponentPreferences.forEach(pref => { + let match = components.find(c => DeviceComponentKey(device, c) === pref.key); + if (match && pref.preferences) { + componentPreferences[match.locationString()] = pref.preferences; + } + }); + let deviceCopy = cloneDeep(device.settings); + deviceCopy.deviceId = 0; + deviceCopy.platform = pond.DevicePlatform.DEVICE_PLATFORM_INVALID; + if (rDevicePreferences.length < 1) { + rDevicePreferences.push(pond.ModelPreferences.create()); + } + let backpack = pond.BackpackSettings.create({ + backpackId: targetBackpackID, + product: product, + name: backpackName, + device: deviceCopy, + components: components.map(c => { + let component = cloneDeep(c.settings); + component.key = ""; + return component; + }), + interactions: interactions.map(i => { + let interaction = cloneDeep(i.settings); + interaction.key = ""; + return interaction; + }), + tagKeys: tagIds, + devicePreferences: rDevicePreferences[0].preferences, + componentPreferences: componentPreferences + }); + resolve(backpack); + }) + .catch(() => { + let reason = "Failed loading user preferences"; + error(reason); + reject(reason); + }); + }); + }; + + const submit = () => { + const { addBackpack, updateBackpack } = backpackAPI; + assembleBackpack().then((backpack: pond.BackpackSettings) => { + if (isNew) { + addBackpack(backpack) + .then((response: any) => { + success("Successfully created " + backpackName + " using " + device.name() + "!"); + refreshCallback(); + }) + .catch((err: any) => { + error(err ? err : "Error occured while creating " + backpackName + "."); + }) + .finally(() => close()); + } else { + updateBackpack(targetBackpackID, backpack) + .then((response: any) => { + success("Successfully updated " + backpackName + " using " + device.name() + "!"); + refreshCallback(); + }) + .catch((err: any) => { + error(err ? err : "Error occured while updating " + backpackName + "."); + }) + .finally(() => close()); + } + }); + }; + + const removeSelectedProfile = () => { + const { removeBackpack } = backpackAPI; + + const backpackID = targetBackpackID; + const backpack = backpacks.find(b => b.id() === backpackID); + if (targetBackpackID && backpack) { + let backpackName = backpack.name(); + removeBackpack(backpackID) + .then(() => { + success("Successfully removed " + backpackName); + let updatedBackpacks = cloneDeep(backpacks); + defaultState(); + setBackpacks(updatedBackpacks.filter(b => b.id() !== backpackID)); + }) + .catch(err => error(err ? err : "Error occured while removing " + backpackName)); + } + }; + + const changeTargetBackpack = (option: Option | null) => { + let isNew = option && option.new === true ? true : false; + setIsNew(isNew); + setTargetBackpackID(isNew || !(option && option.value) ? 0 : Number(option.value)); + setBackpackName(option && option.label ? option.label : backpackName); + }; + + const isFormValid = (): boolean => { + const isValidBackpackName = isNew ? backpackName !== "" : true; + const isValidTargetBackpack = isNew ? true : targetBackpackID > 0; + return isValidBackpackName && isValidTargetBackpack; + }; + + // UI Begins + + const content = () => { + const options = backpackOptions(backpacks); + let selected: Option | undefined = options.find( + option => (option.value as number) === targetBackpackID + ); + + return ( + + changeTargetBackpack(option)} + loading={loading} + creatable + /> + setProduct(+e.target.value)} + margin="normal" + fullWidth + variant="outlined"> + {ListDeviceProductDescribers().map(describer => ( + + {describer.label} + + ))} + + + ); + }; + + const actions = () => { + return ( + + + removeSelectedProfile()}> + Delete + + + + + + + + ); + }; + + return ( + + + Save Device Profile + + {device.name()} + + + {content()} + {actions()} + + ); +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 057e5ed..9c73684 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -16,7 +16,7 @@ export { // useMetricAPI, useMineAPI, usePermissionAPI, -// usePreferenceAPI, + usePreferenceAPI, // useSecurity, useSnackbar, // useTagAPI, diff --git a/src/interactions/InteractionSettings.tsx b/src/interactions/InteractionSettings.tsx index 0335093..82fa450 100644 --- a/src/interactions/InteractionSettings.tsx +++ b/src/interactions/InteractionSettings.tsx @@ -1564,17 +1564,17 @@ export default function InteractionSettings(props: Props) { - + {canEdit && ( + - {canEdit && ( - - )} + )} */} ); diff --git a/src/navigation/SideNavigator.tsx b/src/navigation/SideNavigator.tsx index 41bdc65..f387279 100644 --- a/src/navigation/SideNavigator.tsx +++ b/src/navigation/SideNavigator.tsx @@ -37,6 +37,7 @@ import FieldsIcon from "products/AgIcons/FieldsIcon"; import PlaneIcon from "products/AviationIcons/PlaneIcon"; import AirportMapIcon from "products/AviationIcons/AirportMapIcon"; import JobsiteIcon from "products/Construction/JobSiteIcon"; +import { getThemeType } from "theme"; const drawerWidth = 230; @@ -207,7 +208,7 @@ export default function SideNavigator(props: Props) { classes={getClasses("/team")} > - + {open && } @@ -219,7 +220,7 @@ export default function SideNavigator(props: Props) { classes={getClasses("/user")} > - + {open && } diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 9f966c9..22ee8c1 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -51,7 +51,7 @@ export default function DevicePage() { const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false) const loadDevice = () => { - console.log("load device page data") + // console.log("load device page data") if (loading) return setLoading(true) deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => { @@ -120,7 +120,7 @@ export default function DevicePage() { }).catch(err => { setDevice(Device.create()); // setComponents(new Map()); - // setInteractions([]); + setInteractions([]); setAvailablePositions(new Map()); setPermissions([]); setPreferences(pond.UserPreferences.create()); @@ -436,9 +436,11 @@ export default function DevicePage() { permissions={permissions} preferences={preferences} toggleNotificationPreference={toggleNotificationPreference} - refreshCallback={loadDevice} - availablePositions={availablePositions} - availableOffsets={availableOffsets} + refreshCallback={loadDevice} + availablePositions={availablePositions} + availableOffsets={availableOffsets} + components={[...components.values()]} + interactions={interactions} /> diff --git a/src/providers/index.ts b/src/providers/index.ts index a67f3b7..1b0e889 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -24,7 +24,7 @@ export { // useMeasurementsWebsocket, // useMetricAPI, usePermissionAPI, - // usePreferenceAPI, + usePreferenceAPI, useSiteAPI, //TODO: update api with resolve, reject useTagAPI, useTeamAPI, diff --git a/src/providers/pond/deviceAPI.tsx b/src/providers/pond/deviceAPI.tsx index 6cc6cad..1522853 100644 --- a/src/providers/pond/deviceAPI.tsx +++ b/src/providers/pond/deviceAPI.tsx @@ -1,6 +1,6 @@ import { useHTTP, usePermissionAPI } from "hooks"; // import { useWebsocket } from "websocket"; -import { /*Component,*/ deviceScope, User } from "models"; +import { /*Component,*/ Component, deviceScope, User } from "models"; import { pond } from "protobuf-ts/pond"; import { createContext, PropsWithChildren, useContext } from "react"; import { pondURL } from "./pond"; @@ -8,6 +8,7 @@ import { AxiosResponse } from "axios"; import { useGlobalState } from "providers"; import moment from "moment"; import { or } from "utils/types"; +import { dateRange } from "providers/http"; // import { reject, result } from "lodash"; export interface IDeviceAPIContext { @@ -113,16 +114,16 @@ export interface IDeviceAPIContext { isOverLimit: (id: number) => Promise; isPaused: (id: number) => Promise; setDatacap: (id: number, newCap: number) => Promise; - // listJSONMeasurements: ( - // id: number, - // components: Component[], - // startDate: any, - // endDate: any, - // limit: number, - // offset: number, - // order: string, - // orderBy: string - // ) => Promise>; + listJSONMeasurements: ( + id: number, + components: Component[], + startDate: any, + endDate: any, + limit: number, + offset: number, + order: string, + orderBy: string + ) => Promise>; listSimpleJSON: (device: number) => Promise; resetQuackCount: (id: number) => Promise; resetQuackCountTx: (id: number) => Promise; @@ -755,65 +756,65 @@ export default function DeviceProvider(props: PropsWithChildren) { }) }; - // const listJSONMeasurements = ( - // id: number, - // components: Component[], - // startDate: any, - // endDate: any, - // limit: number, - // offset: number, - // order: string, - // orderBy: string - // ) => { - // let keyString = ""; - // components.forEach((comp, i) => { - // if (i === 0) { - // keyString = keyString + comp.key(); - // } else { - // keyString = keyString + "," + comp.key(); - // } - // }); - // if (as) { - // return get( - // pondURL( - // "/devices/" + - // id + - // "/measurements/exportComplex" + - // dateRange(startDate, endDate) + - // "&components=" + - // keyString + - // "&limit=" + - // limit + - // "&offset=" + - // offset + - // "&order=" + - // order + - // "&orderBy=" + - // orderBy + - // "&as=" + - // as - // ) - // ); - // } - // return get( - // pondURL( - // "/devices/" + - // id + - // "/measurements/exportComplex" + - // dateRange(startDate, endDate) + - // "&components=" + - // keyString + - // "&limit=" + - // limit + - // "&offset=" + - // offset + - // "&order=" + - // order + - // "&orderBy=" + - // orderBy - // ) - // ); - // }; + const listJSONMeasurements = ( + id: number, + components: Component[], + startDate: any, + endDate: any, + limit: number, + offset: number, + order: string, + orderBy: string + ) => { + let keyString = ""; + components.forEach((comp, i) => { + if (i === 0) { + keyString = keyString + comp.key(); + } else { + keyString = keyString + "," + comp.key(); + } + }); + if (as) { + return get( + pondURL( + "/devices/" + + id + + "/measurements/exportComplex" + + dateRange(startDate, endDate) + + "&components=" + + keyString + + "&limit=" + + limit + + "&offset=" + + offset + + "&order=" + + order + + "&orderBy=" + + orderBy + + "&as=" + + as + ) + ); + } + return get( + pondURL( + "/devices/" + + id + + "/measurements/exportComplex" + + dateRange(startDate, endDate) + + "&components=" + + keyString + + "&limit=" + + limit + + "&offset=" + + offset + + "&order=" + + order + + "&orderBy=" + + orderBy + ) + ); + }; const listSimpleJSON = (device: number) => { return new Promise((resolve, reject) => { @@ -925,7 +926,7 @@ export default function DeviceProvider(props: PropsWithChildren) { isOverLimit, isPaused, setDatacap, - // listJSONMeasurements, + listJSONMeasurements, listSimpleJSON, resetQuackCount, resetQuackCountTx, diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index 6f7735b..5a3d840 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -28,6 +28,7 @@ import TerminalProvider, {useTerminalAPI} from "./terminalAPI" import SiteProvider, {useSiteAPI} from "./siteAPI"; import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI"; import MutationProvider, {useMutationAPI} from "./mutationAPI" +import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -72,7 +73,9 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + {children} + @@ -131,5 +134,6 @@ export { useTerminalAPI, useSiteAPI, useObjectHeaterAPI, - useMutationAPI + useMutationAPI, + usePreferenceAPI }; diff --git a/src/providers/pond/preferenceAPI.tsx b/src/providers/pond/preferenceAPI.tsx new file mode 100644 index 0000000..91ed9b7 --- /dev/null +++ b/src/providers/pond/preferenceAPI.tsx @@ -0,0 +1,49 @@ +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { pondURL } from "./pond"; + +export interface IPreferenceAPIContext { + getPreferences: (kind: string, keys: string[]) => Promise; +} + +export const PreferenceAPIContext = createContext( + {} as IPreferenceAPIContext +); + +interface Props {} + +export default function PreferenceProvider(props: PropsWithChildren) { + const { children } = props; + const { get } = useHTTP(); + + const getPreferences = (kind: string, keys: string[]): Promise => { + if (keys.length <= 0) { + return Promise.resolve([]); + } + return new Promise((resolve, reject) => { + get(pondURL("/preferences/?kind=" + kind + "&keys=" + keys.join(","))) + .then((res: any) => { + let prefs: pond.ModelPreferences[] = []; + if (res && res.data && res.data.preferences) { + res.data.preferences.forEach((raw: any) => { + prefs.push(pond.ModelPreferences.create(raw)); + }); + } + resolve(prefs); + }) + .catch((err: any) => reject(err)); + }); + }; + + return ( + + {children} + + ); +} + +export const usePreferenceAPI = () => useContext(PreferenceAPIContext);