From 59d9bf2e6ba2325368b5b3d7b9851793539a102b Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 2 Apr 2025 15:17:55 -0600 Subject: [PATCH] added some more device actions including interactions stuff --- src/bin/BinYards.tsx | 4 +- src/common/DraggableTabs.tsx | 5 - src/device/DeviceActions.tsx | 16 +- src/device/SaveDeviceProfile.tsx | 297 +++++++++++++++++++++++ src/hooks/index.ts | 2 +- src/interactions/InteractionSettings.tsx | 20 +- src/pages/Device.tsx | 12 +- src/providers/index.ts | 2 +- src/providers/pond/pond.tsx | 8 +- src/providers/pond/preferenceAPI.tsx | 49 ++++ 10 files changed, 381 insertions(+), 34 deletions(-) create mode 100644 src/device/SaveDeviceProfile.tsx create mode 100644 src/providers/pond/preferenceAPI.tsx diff --git a/src/bin/BinYards.tsx b/src/bin/BinYards.tsx index 8c11242..0d371f0 100644 --- a/src/bin/BinYards.tsx +++ b/src/bin/BinYards.tsx @@ -210,12 +210,11 @@ 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) { - console.log(props.yards) setBinYards(props.yards); setYardPermissions(props.yardPerms); return; @@ -249,7 +248,6 @@ export default function BinYard(props: Props) { setBinYards(newYards) setShowAddYard(false); newBinYard.key = resp.data.yard - console.log(resp.data.yard) let newYardPermissions: Dictionary = { ...yardPermissions }; newYardPermissions[newBinYard.key] = [ diff --git a/src/common/DraggableTabs.tsx b/src/common/DraggableTabs.tsx index 4dc8b30..0021a18 100644 --- a/src/common/DraggableTabs.tsx +++ b/src/common/DraggableTabs.tsx @@ -75,14 +75,9 @@ export default function DraggableTabs(props: DraggableTabsProps) { // save tab order to cache when it changes useEffect(() => { - console.log(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 : ""; } diff --git a/src/device/DeviceActions.tsx b/src/device/DeviceActions.tsx index 4e969df..01f2330 100644 --- a/src/device/DeviceActions.tsx +++ b/src/device/DeviceActions.tsx @@ -42,7 +42,7 @@ import SyncDevice from "device/SyncDevice"; // 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,8 @@ 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"; const useStyles = makeStyles((_theme: Theme) => { // const isMobile = useMobile() @@ -97,7 +99,7 @@ interface Props { device: Device; isPaused: boolean; components: Component[]; - // interactions: Interaction[]; + interactions: Interaction[]; refreshCallback: () => void; availablePositions: DeviceAvailabilityMap; availableOffsets: OffsetAvailabilityMap; @@ -130,7 +132,7 @@ export default function DeviceActions(props: Props) { device, isPaused, components, - // interactions, + interactions, refreshCallback, availablePositions, availableOffsets, @@ -409,7 +411,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,7 +450,7 @@ export default function DeviceActions(props: Props) { interactions={interactions} tagIds={device.status.tagKeys} refreshCallback={refreshCallback} - /> */} + /> closeDialog("isLoadDeviceProfileOpen")} 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/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 305e7f1..9a1fc2e 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/pond.tsx b/src/providers/pond/pond.tsx index 779b4a0..aff9abe 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -26,6 +26,7 @@ import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI"; import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI"; import TerminalProvider, {useTerminalAPI} from "./terminalAPI" import SiteProvider, {useSiteAPI} from "./siteAPI"; +import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -68,7 +69,9 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + {children} + @@ -123,5 +126,6 @@ export { useHomeMarkerAPI, useHarvestPlanAPI, useTerminalAPI, - useSiteAPI + useSiteAPI, + 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);