Merge branch 'dev_environment' of gitlab.com:brandx/bxt-app into dev_environment

This commit is contained in:
csawatzky 2025-04-03 12:55:40 -06:00
commit 5065c07820
18 changed files with 972 additions and 125 deletions

View file

@ -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<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) {
@ -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<pond.Permission[]> = { ...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) {
<DialogTitle>Create New Bin Yard</DialogTitle>
{yardForm()}
<DialogActions>
<Button
onClick={() => {
setShowAddYard(false);
}}
color="primary">
Cancel
</Button>
<Button onClick={submitYard} color="primary">
Submit
</Button>
<CancelSubmit
submitDisabled={addingYard}
onCancel={() => setShowAddYard(false)}
onSubmit={submitYard}
/>
</DialogActions>
</ResponsiveDialog>
);

View file

@ -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]

View file

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

View file

@ -74,7 +74,7 @@ interface ListProps {
close: (refresh: boolean) => void;
device: Device;
components: Component[];
devicePreferences: pond.UserPreferences;
devicePreferences: pond.DevicePreferences;
}
const filteredComponents = (components: Component[]) => {

View file

@ -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<Moment>(GetDefaultDateRange().start);
const [endDate, setEndDate] = useState<Moment>(GetDefaultDateRange().end);
const [includedComponents, setIncludedComponents] = useState<Component[]>([]);
const [filename, setFilename] = useState<string>(GetDefaultDateRange().start.toString());
const [limit, setLimit] = useState<number>(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 => (
<FormControlLabel
label={comp.name()}
key={comp.key()}
control={
<Checkbox onChange={e => 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 (
<ResponsiveDialog open={open} onClose={handleClose}>
<DialogTitle>Export JSON Data for Device</DialogTitle>
<DialogContent>
<FormControlLabel
label={"Simple Export"}
control={
<Checkbox
value={useFlat}
onChange={e => setUseFlat(e.target.checked)}
color="primary"
/>
}
/>
<Divider />
{!useFlat && (
<React.Fragment>
<TextField
id="filename"
fullWidth
variant="outlined"
value={filename}
label="Filename"
onChange={e => setFilename(e.target.value)}
InputProps={{
endAdornment: <InputAdornment position="end">.json</InputAdornment>
}}
/>
<DateSelect
startDate={startDate}
endDate={endDate}
updateDateRange={updateDateRange}
label="Extract data from"
/>
<TextField
id="limit"
fullWidth
variant="outlined"
label="Maximum number of measurements per component"
type="number"
value={limit}
onChange={e => setLimit(+e.target.value)}
/>
{componentSelector()}
</React.Fragment>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose} style={{ color: "red" }}>
Cancel
</Button>
<Button onClick={getMeasurements} color="primary" disabled={!isFormValid()}>
Get Measurements
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

148
src/device/Connection.tsx Normal file
View file

@ -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<string>("");
const [password, setPassword] = useState<string>("");
const [passwordVisible, setPasswordVisible] = useState<boolean>(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 (
<ResponsiveDialog
maxWidth="sm"
fullWidth
open={open}
onClose={() => handleClose()}
aria-labelledby="device-connection">
<DialogTitle id="device-connection-title">
Connection
<Typography variant="body2" color="textSecondary">
{deviceName}
</Typography>
</DialogTitle>
<DialogContent>
<Typography>
By default your Wi-Fi enabled device will try to connect to the gateway{" "}
<span className={classes.highlight}>device</span> with the password{" "}
<span className={classes.highlight}>configure</span>. Your device must be connected to the
internet to change its Wi-Fi credentials.
</Typography>
<TextField
id="gateway"
name="gateway"
label="Gateway"
value={gateway}
onChange={event => setGateway(event.target.value)}
error={!isGatewayValid()}
helperText={isGatewayValid() ? "" : "Cannot be more than 32 characters"}
margin="normal"
fullWidth
variant="outlined"
/>
<TextField
type={passwordVisible ? "text" : "password"}
id="password"
name="password"
label="Password"
value={password}
onChange={event => setPassword(event.target.value)}
error={!isPasswordValid()}
helperText={isPasswordValid() ? "" : "Cannot be more than 32 characters"}
margin="normal"
fullWidth
variant="outlined"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setPasswordVisible(!passwordVisible)}>
{passwordVisible ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => handleClose()} color="primary">
Cancel
</Button>
<Button onClick={() => handleSubmit()} color="primary" disabled={!isConnectionValid()}>
Submit
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -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")}
/>
{/* <InteractionSettings
<InteractionSettings
device={device}
components={cloneDeep(components)}
mode="add"
@ -417,7 +422,7 @@ export default function DeviceActions(props: Props) {
closeDialogCallback={() => closeDialog("isInteractionSettingsOpen")}
refreshCallback={refreshCallback}
canEdit={canWrite}
/> */}
/>
<ObjectUsers
scope={deviceScope(device.id().toString())}
label={deviceName}
@ -440,7 +445,7 @@ export default function DeviceActions(props: Props) {
isDialogOpen={isRemoveSelfDialogOpen}
closeDialogCallback={() => closeDialog("isRemoveSelfDialogOpen")}
/>
{/* <SaveDeviceProfile
<SaveDeviceProfile
isDialogOpen={isSaveDeviceProfileOpen}
closeDialogCallback={() => closeDialog("isSaveDeviceProfileOpen")}
device={device}
@ -448,14 +453,14 @@ export default function DeviceActions(props: Props) {
interactions={interactions}
tagIds={device.status.tagKeys}
refreshCallback={refreshCallback}
/> */}
/>
<LoadDeviceProfile
isDialogOpen={isLoadDeviceProfileOpen}
closeDialogCallback={() => closeDialog("isLoadDeviceProfileOpen")}
device={device}
refreshCallback={refreshCallback}
/>
{/* <PauseData
<PauseData
id={device.id()}
isOpen={isPauseDialogOpen}
close={closeAndRefresh("isPauseDialogOpen")}
@ -483,7 +488,7 @@ export default function DeviceActions(props: Props) {
close={closeAndRefresh("isJsonDataDialogOpen")}
device={device}
components={components}
/> */}
/>
</React.Fragment>
);
};

71
src/device/PauseData.tsx Normal file
View file

@ -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 (
<Dialog
fullWidth
open={isOpen}
onClose={() => close(false)}
aria-labelledby="pause-data-dialog">
<DialogTitle id="pause-data-dialog-title">Pause Data</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
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.
<br />
<br />
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.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => close(false)} color="primary">
Close
</Button>
<Button onClick={pause} color="primary">
Pause
</Button>
</DialogActions>
</Dialog>
);
}

66
src/device/ResumeData.tsx Normal file
View file

@ -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 (
<Dialog
fullWidth
open={isOpen}
onClose={() => close(false)}
aria-labelledby="resume-data-dialog">
<DialogTitle id="resume-data-dialog-title">Resume Data</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
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.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => close(false)} color="primary">
Close
</Button>
<Button onClick={resume} color="primary">
Resume
</Button>
</DialogActions>
</Dialog>
);
}

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,7 +1564,7 @@ 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">
{/* <Button onClick={close} color="primary">
Cancel
</Button>
{canEdit && (
@ -1574,7 +1574,7 @@ export default function InteractionSettings(props: Props) {
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
Submit
</Button>
)}
)} */}
</Grid>
</Grid>
);

View file

@ -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")}
>
<ListItemIcon>
<People />
<People style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
</ListItemIcon>
{open && <ListItemText primary="Teams" />}
</ListItemButton>
@ -219,7 +220,7 @@ export default function SideNavigator(props: Props) {
classes={getClasses("/user")}
>
<ListItemIcon>
<Person />
<Person style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
</ListItemIcon>
{open && <ListItemText primary="Users" />}
</ListItemButton>

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());
@ -439,6 +439,8 @@ export default function DevicePage() {
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

@ -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<any>;
isPaused: (id: number) => Promise<any>;
setDatacap: (id: number, newCap: number) => Promise<any>;
// listJSONMeasurements: (
// id: number,
// components: Component[],
// startDate: any,
// endDate: any,
// limit: number,
// offset: number,
// order: string,
// orderBy: string
// ) => Promise<AxiosResponse<pond.ListDeviceExportedMeasurementsResponse>>;
listJSONMeasurements: (
id: number,
components: Component[],
startDate: any,
endDate: any,
limit: number,
offset: number,
order: string,
orderBy: string
) => Promise<AxiosResponse<pond.ListDeviceExportedMeasurementsResponse>>;
listSimpleJSON: (device: number) => Promise<any>;
resetQuackCount: (id: number) => Promise<any>;
resetQuackCountTx: (id: number) => Promise<any>;
@ -755,65 +756,65 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
})
};
// 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<pond.ListDeviceExportedMeasurementsResponse>(
// pondURL(
// "/devices/" +
// id +
// "/measurements/exportComplex" +
// dateRange(startDate, endDate) +
// "&components=" +
// keyString +
// "&limit=" +
// limit +
// "&offset=" +
// offset +
// "&order=" +
// order +
// "&orderBy=" +
// orderBy +
// "&as=" +
// as
// )
// );
// }
// return get<pond.ListDeviceExportedMeasurementsResponse>(
// 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<pond.ListDeviceExportedMeasurementsResponse>(
pondURL(
"/devices/" +
id +
"/measurements/exportComplex" +
dateRange(startDate, endDate) +
"&components=" +
keyString +
"&limit=" +
limit +
"&offset=" +
offset +
"&order=" +
order +
"&orderBy=" +
orderBy +
"&as=" +
as
)
);
}
return get<pond.ListDeviceExportedMeasurementsResponse>(
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<AxiosResponse>((resolve, reject) => {
@ -925,7 +926,7 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
isOverLimit,
isPaused,
setDatacap,
// listJSONMeasurements,
listJSONMeasurements,
listSimpleJSON,
resetQuackCount,
resetQuackCountTx,

View file

@ -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<any>) {
<SiteProvider>
<ObjectHeaterProvider>
<MutationProvider>
<PreferenceProvider>
{children}
</PreferenceProvider>
</MutationProvider>
</ObjectHeaterProvider>
</SiteProvider>
@ -131,5 +134,6 @@ export {
useTerminalAPI,
useSiteAPI,
useObjectHeaterAPI,
useMutationAPI
useMutationAPI,
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);