added some more device actions including interactions stuff

This commit is contained in:
Carter 2025-04-02 15:17:55 -06:00
parent e4cc9b3554
commit 59d9bf2e6b
10 changed files with 381 additions and 34 deletions

View file

@ -210,12 +210,11 @@ export default function BinYard(props: Props) {
const [leaving, setLeaving] = useState<boolean>(false);
const [yardPermissions, setYardPermissions] = useState<Dictionary<pond.Permission[]>>({});
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<pond.Permission[]> = { ...yardPermissions };
newYardPermissions[newBinYard.key] = [

View file

@ -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 : "";
}

View file

@ -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")}
/>
{/* <InteractionSettings
<InteractionSettings
device={device}
components={cloneDeep(components)}
mode="add"
@ -417,7 +419,7 @@ export default function DeviceActions(props: Props) {
closeDialogCallback={() => closeDialog("isInteractionSettingsOpen")}
refreshCallback={refreshCallback}
canEdit={canWrite}
/> */}
/>
<ObjectUsers
scope={deviceScope(device.id().toString())}
label={deviceName}
@ -440,7 +442,7 @@ export default function DeviceActions(props: Props) {
isDialogOpen={isRemoveSelfDialogOpen}
closeDialogCallback={() => closeDialog("isRemoveSelfDialogOpen")}
/>
{/* <SaveDeviceProfile
<SaveDeviceProfile
isDialogOpen={isSaveDeviceProfileOpen}
closeDialogCallback={() => closeDialog("isSaveDeviceProfileOpen")}
device={device}
@ -448,7 +450,7 @@ export default function DeviceActions(props: Props) {
interactions={interactions}
tagIds={device.status.tagKeys}
refreshCallback={refreshCallback}
/> */}
/>
<LoadDeviceProfile
isDialogOpen={isLoadDeviceProfileOpen}
closeDialogCallback={() => closeDialog("isLoadDeviceProfileOpen")}

View file

@ -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<Backpack[]>([]);
const [backpackName, setBackpackName] = useState<string>();
const [loading, setLoading] = useState<boolean>(false);
const [isNew, setIsNew] = useState<boolean>(false);
const [targetBackpackID, setTargetBackpackID] = useState<number>(0);
const [product, setProduct] = useState<pond.DeviceProduct>(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<pond.BackpackSettings> => {
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 (
<React.Fragment>
<SearchSelect
label={"Save to an existing or new profile"}
selected={selected}
options={options}
changeSelection={(option: Option | null) => changeTargetBackpack(option)}
loading={loading}
creatable
/>
<TextField
id="deviceProduct"
label="Device Product"
select
value={product}
onChange={e => setProduct(+e.target.value)}
margin="normal"
fullWidth
variant="outlined">
{ListDeviceProductDescribers().map(describer => (
<MenuItem key={describer.product} value={describer.product}>
{describer.label}
</MenuItem>
))}
</TextField>
</React.Fragment>
);
};
const actions = () => {
return (
<Grid container justifyContent="space-between" direction="row">
<Grid size={{ xs: 5 }}>
<DeleteButton disabled={!isFormValid()} onClick={() => removeSelectedProfile()}>
Delete
</DeleteButton>
</Grid>
<Grid size={{ xs: 7 }} container justifyItems="flex-end">
<Button size="small" color="primary" onClick={close}>
Cancel
</Button>
<Button size="small" disabled={!isFormValid()} color="primary" onClick={submit}>
{isNew ? "Create" : "Update"}
</Button>
</Grid>
</Grid>
);
};
return (
<ResponsiveDialog
open={isDialogOpen}
onClose={close}
aria-labelledby="save-device-profile"
maxWidth="sm"
fullWidth>
<DialogTitle id="save-device-profile-title">
Save Device Profile
<Typography variant="body2" color="textSecondary">
{device.name()}
</Typography>
</DialogTitle>
<DialogContent className={classes.contentSpacing}>{content()}</DialogContent>
<DialogActions>{actions()}</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -16,7 +16,7 @@ export {
// useMetricAPI,
useMineAPI,
usePermissionAPI,
// usePreferenceAPI,
usePreferenceAPI,
// useSecurity,
useSnackbar,
// useTagAPI,

View file

@ -1564,17 +1564,17 @@ export default function InteractionSettings(props: Props) {
</Grid>
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
<Button onClick={close} color="primary">
Cancel
{/* <Button onClick={close} color="primary">
Cancel
</Button>
{canEdit && (
<Button
onClick={submit}
color="primary"
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
Submit
</Button>
{canEdit && (
<Button
onClick={submit}
color="primary"
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
Submit
</Button>
)}
)} */}
</Grid>
</Grid>
);

View file

@ -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}
/>
</Grid>
</Grid>

View file

@ -24,7 +24,7 @@ export {
// useMeasurementsWebsocket,
// useMetricAPI,
usePermissionAPI,
// usePreferenceAPI,
usePreferenceAPI,
useSiteAPI, //TODO: update api with resolve, reject
useTagAPI,
useTeamAPI,

View file

@ -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<any>) {
<HarvestPlanProvider>
<TerminalProvider>
<SiteProvider>
{children}
<PreferenceProvider>
{children}
</PreferenceProvider>
</SiteProvider>
</TerminalProvider>
</HarvestPlanProvider>
@ -123,5 +126,6 @@ export {
useHomeMarkerAPI,
useHarvestPlanAPI,
useTerminalAPI,
useSiteAPI
useSiteAPI,
usePreferenceAPI
};

View file

@ -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<pond.ModelPreferences[]>;
}
export const PreferenceAPIContext = createContext<IPreferenceAPIContext>(
{} as IPreferenceAPIContext
);
interface Props {}
export default function PreferenceProvider(props: PropsWithChildren<Props>) {
const { children } = props;
const { get } = useHTTP();
const getPreferences = (kind: string, keys: string[]): Promise<pond.ModelPreferences[]> => {
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 (
<PreferenceAPIContext.Provider
value={{
getPreferences
}}>
{children}
</PreferenceAPIContext.Provider>
);
}
export const usePreferenceAPI = () => useContext(PreferenceAPIContext);