Merge branch 'dev_environment' of gitlab.com:brandx/bxt-app into dev_environment
This commit is contained in:
commit
5065c07820
18 changed files with 972 additions and 125 deletions
|
|
@ -50,6 +50,7 @@ import ObjectTeams from "teams/ObjectTeams";
|
||||||
import { GrainBag } from "models/GrainBag";
|
import { GrainBag } from "models/GrainBag";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
import DraggableTabs from "common/DraggableTabs";
|
import DraggableTabs from "common/DraggableTabs";
|
||||||
|
import CancelSubmit from "common/CancelSubmit";
|
||||||
|
|
||||||
const parentTab = {
|
const parentTab = {
|
||||||
"&": {
|
"&": {
|
||||||
|
|
@ -209,7 +210,8 @@ export default function BinYard(props: Props) {
|
||||||
const [leaving, setLeaving] = useState<boolean>(false);
|
const [leaving, setLeaving] = useState<boolean>(false);
|
||||||
const [yardPermissions, setYardPermissions] = useState<Dictionary<pond.Permission[]>>({});
|
const [yardPermissions, setYardPermissions] = useState<Dictionary<pond.Permission[]>>({});
|
||||||
const [searchSelected, setSearchSelected] = useState(false);
|
const [searchSelected, setSearchSelected] = useState(false);
|
||||||
const [{ user, as }] = useGlobalState();
|
const [{ user }] = useGlobalState();
|
||||||
|
const [addingYard, setAddingYard] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.yards && props.yardPerms) {
|
if (props.yards && props.yardPerms) {
|
||||||
|
|
@ -231,22 +233,36 @@ export default function BinYard(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitYard = () => {
|
const submitYard = () => {
|
||||||
|
if (addingYard) return
|
||||||
|
setAddingYard(true)
|
||||||
let newBinYard = pond.BinYardSettings.create();
|
let newBinYard = pond.BinYardSettings.create();
|
||||||
newBinYard.name = addYardName;
|
newBinYard.name = addYardName;
|
||||||
newBinYard.description = addYardDescription;
|
newBinYard.description = addYardDescription;
|
||||||
newBinYard.owner = user.id();
|
newBinYard.owner = user.id();
|
||||||
binYardAPI
|
binYardAPI
|
||||||
.addBinYard(newBinYard)
|
.addBinYard(newBinYard)
|
||||||
.then(_resp => {
|
.then(resp => {
|
||||||
// loadYards();
|
// loadYards();
|
||||||
let newYards = [...binYards]
|
let newYards = [...binYards]
|
||||||
newYards.push(newBinYard)
|
newYards.push(newBinYard)
|
||||||
setBinYards(newYards)
|
setBinYards(newYards)
|
||||||
setShowAddYard(false);
|
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 :)");
|
openSnackbar("success", "Bin Yard created :)");
|
||||||
})
|
})
|
||||||
.catch(_err => {
|
.catch(_err => {
|
||||||
error("Could not add bin yard");
|
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>
|
<DialogTitle>Create New Bin Yard</DialogTitle>
|
||||||
{yardForm()}
|
{yardForm()}
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<CancelSubmit
|
||||||
onClick={() => {
|
submitDisabled={addingYard}
|
||||||
setShowAddYard(false);
|
onCancel={() => setShowAddYard(false)}
|
||||||
}}
|
onSubmit={submitYard}
|
||||||
color="primary">
|
/>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={submitYard} color="primary">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,52 @@ export default function DraggableTabs(props: DraggableTabsProps) {
|
||||||
return storedValue !== null ? JSON.parse(storedValue) : [];
|
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
|
// save tab order to cache when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(tabOrder)
|
localStorage.setItem(cacheKey, JSON.stringify(sanitize(tabOrder)));
|
||||||
localStorage.setItem(cacheKey, JSON.stringify(tabOrder));
|
|
||||||
}, [tabOrder]);
|
}, [tabOrder]);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// console.log(initialChildren.length)
|
|
||||||
// }, [initialChildren])
|
|
||||||
|
|
||||||
const getKey = (node: ReactNode): string => {
|
const getKey = (node: ReactNode): string => {
|
||||||
return (React.isValidElement(node) && 'key' in node && typeof node.key === 'string') ? node.key : "";
|
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
|
// Find the missing key
|
||||||
const removedIndex = oldKeys.findIndex(key => !newKeys.includes(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) {
|
} else if (tabOrder.length - initialChildren.length === -1) {
|
||||||
let newTabOrder = [...tabOrder]
|
let newTabOrder = [...tabOrder]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Icon, Theme } from "@mui/material";
|
import { Icon, Theme } from "@mui/material";
|
||||||
import { makeStyles } from "@mui/styles";
|
import { makeStyles } from "@mui/styles";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme: Theme) => ({
|
const useStyles = makeStyles((_theme: Theme) => ({
|
||||||
icon: {
|
icon: {
|
||||||
display: "flex", // Ensure flexbox for centering
|
display: "flex", // Ensure flexbox for centering
|
||||||
justifyContent: "center", // Center horizontally
|
justifyContent: "center", // Center horizontally
|
||||||
|
|
@ -14,7 +14,6 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
maxWidth: "100%", // Fit within container width
|
maxWidth: "100%", // Fit within container width
|
||||||
width: "auto", // Maintain aspect ratio
|
width: "auto", // Maintain aspect ratio
|
||||||
height: "auto", // Maintain aspect ratio
|
height: "auto", // Maintain aspect ratio
|
||||||
color: "black"
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ interface ListProps {
|
||||||
close: (refresh: boolean) => void;
|
close: (refresh: boolean) => void;
|
||||||
device: Device;
|
device: Device;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
devicePreferences: pond.UserPreferences;
|
devicePreferences: pond.DevicePreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredComponents = (components: Component[]) => {
|
const filteredComponents = (components: Component[]) => {
|
||||||
|
|
|
||||||
157
src/device/ArcGISDeviceData.tsx
Normal file
157
src/device/ArcGISDeviceData.tsx
Normal 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
148
src/device/Connection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -34,15 +34,15 @@ import NotificationButton from "common/NotificationButton";
|
||||||
// import ComponentSettings from "component/ComponentSettings";
|
// import ComponentSettings from "component/ComponentSettings";
|
||||||
// import DeviceSettings from "device/DeviceSettings";
|
// import DeviceSettings from "device/DeviceSettings";
|
||||||
// import LoadDeviceProfile from "device/LoadDeviceProfile";
|
// import LoadDeviceProfile from "device/LoadDeviceProfile";
|
||||||
// import { PauseData } from "device/PauseData";
|
import { PauseData } from "device/PauseData";
|
||||||
// import { ResumeData } from "device/ResumeData";
|
import { ResumeData } from "device/ResumeData";
|
||||||
// import SaveDeviceProfile from "device/SaveDeviceProfile";
|
// import SaveDeviceProfile from "device/SaveDeviceProfile";
|
||||||
import SyncDevice from "device/SyncDevice";
|
import SyncDevice from "device/SyncDevice";
|
||||||
// import UpgradeDevice from "device/UpgradeDevice";
|
// import UpgradeDevice from "device/UpgradeDevice";
|
||||||
// import InteractionSettings from "interactions/InteractionSettings";
|
// import InteractionSettings from "interactions/InteractionSettings";
|
||||||
import { cloneDeep } from "lodash";
|
import { cloneDeep } from "lodash";
|
||||||
// import { Component, Device, deviceScope, Interaction } from "models";
|
// 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 { MatchParams } from "navigation/Routes";
|
||||||
// import { isShareableLink } from "pbHelpers/Device";
|
// import { isShareableLink } from "pbHelpers/Device";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
|
|
@ -65,6 +65,11 @@ import DeviceSettings from "device/DeviceSettings";
|
||||||
import ObjectTeams from "teams/ObjectTeams";
|
import ObjectTeams from "teams/ObjectTeams";
|
||||||
import ComponentSettings from "component/ComponentSettings";
|
import ComponentSettings from "component/ComponentSettings";
|
||||||
import LoadDeviceProfile from "./LoadDeviceProfile";
|
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 useStyles = makeStyles((_theme: Theme) => {
|
||||||
// const isMobile = useMobile()
|
// const isMobile = useMobile()
|
||||||
|
|
@ -97,7 +102,7 @@ interface Props {
|
||||||
device: Device;
|
device: Device;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
// interactions: Interaction[];
|
interactions: Interaction[];
|
||||||
refreshCallback: () => void;
|
refreshCallback: () => void;
|
||||||
availablePositions: DeviceAvailabilityMap;
|
availablePositions: DeviceAvailabilityMap;
|
||||||
availableOffsets: OffsetAvailabilityMap;
|
availableOffsets: OffsetAvailabilityMap;
|
||||||
|
|
@ -130,7 +135,7 @@ export default function DeviceActions(props: Props) {
|
||||||
device,
|
device,
|
||||||
isPaused,
|
isPaused,
|
||||||
components,
|
components,
|
||||||
// interactions,
|
interactions,
|
||||||
refreshCallback,
|
refreshCallback,
|
||||||
availablePositions,
|
availablePositions,
|
||||||
availableOffsets,
|
availableOffsets,
|
||||||
|
|
@ -409,7 +414,7 @@ export default function DeviceActions(props: Props) {
|
||||||
isDialogOpen={or(isShareObjectDialogOpen, false)}
|
isDialogOpen={or(isShareObjectDialogOpen, false)}
|
||||||
closeDialogCallback={() => closeDialog("isShareObjectDialogOpen")}
|
closeDialogCallback={() => closeDialog("isShareObjectDialogOpen")}
|
||||||
/>
|
/>
|
||||||
{/* <InteractionSettings
|
<InteractionSettings
|
||||||
device={device}
|
device={device}
|
||||||
components={cloneDeep(components)}
|
components={cloneDeep(components)}
|
||||||
mode="add"
|
mode="add"
|
||||||
|
|
@ -417,7 +422,7 @@ export default function DeviceActions(props: Props) {
|
||||||
closeDialogCallback={() => closeDialog("isInteractionSettingsOpen")}
|
closeDialogCallback={() => closeDialog("isInteractionSettingsOpen")}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
canEdit={canWrite}
|
canEdit={canWrite}
|
||||||
/> */}
|
/>
|
||||||
<ObjectUsers
|
<ObjectUsers
|
||||||
scope={deviceScope(device.id().toString())}
|
scope={deviceScope(device.id().toString())}
|
||||||
label={deviceName}
|
label={deviceName}
|
||||||
|
|
@ -440,7 +445,7 @@ export default function DeviceActions(props: Props) {
|
||||||
isDialogOpen={isRemoveSelfDialogOpen}
|
isDialogOpen={isRemoveSelfDialogOpen}
|
||||||
closeDialogCallback={() => closeDialog("isRemoveSelfDialogOpen")}
|
closeDialogCallback={() => closeDialog("isRemoveSelfDialogOpen")}
|
||||||
/>
|
/>
|
||||||
{/* <SaveDeviceProfile
|
<SaveDeviceProfile
|
||||||
isDialogOpen={isSaveDeviceProfileOpen}
|
isDialogOpen={isSaveDeviceProfileOpen}
|
||||||
closeDialogCallback={() => closeDialog("isSaveDeviceProfileOpen")}
|
closeDialogCallback={() => closeDialog("isSaveDeviceProfileOpen")}
|
||||||
device={device}
|
device={device}
|
||||||
|
|
@ -448,14 +453,14 @@ export default function DeviceActions(props: Props) {
|
||||||
interactions={interactions}
|
interactions={interactions}
|
||||||
tagIds={device.status.tagKeys}
|
tagIds={device.status.tagKeys}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
/> */}
|
/>
|
||||||
<LoadDeviceProfile
|
<LoadDeviceProfile
|
||||||
isDialogOpen={isLoadDeviceProfileOpen}
|
isDialogOpen={isLoadDeviceProfileOpen}
|
||||||
closeDialogCallback={() => closeDialog("isLoadDeviceProfileOpen")}
|
closeDialogCallback={() => closeDialog("isLoadDeviceProfileOpen")}
|
||||||
device={device}
|
device={device}
|
||||||
refreshCallback={refreshCallback}
|
refreshCallback={refreshCallback}
|
||||||
/>
|
/>
|
||||||
{/* <PauseData
|
<PauseData
|
||||||
id={device.id()}
|
id={device.id()}
|
||||||
isOpen={isPauseDialogOpen}
|
isOpen={isPauseDialogOpen}
|
||||||
close={closeAndRefresh("isPauseDialogOpen")}
|
close={closeAndRefresh("isPauseDialogOpen")}
|
||||||
|
|
@ -483,7 +488,7 @@ export default function DeviceActions(props: Props) {
|
||||||
close={closeAndRefresh("isJsonDataDialogOpen")}
|
close={closeAndRefresh("isJsonDataDialogOpen")}
|
||||||
device={device}
|
device={device}
|
||||||
components={components}
|
components={components}
|
||||||
/> */}
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
71
src/device/PauseData.tsx
Normal file
71
src/device/PauseData.tsx
Normal 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
66
src/device/ResumeData.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
297
src/device/SaveDeviceProfile.tsx
Normal file
297
src/device/SaveDeviceProfile.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ export {
|
||||||
// useMetricAPI,
|
// useMetricAPI,
|
||||||
useMineAPI,
|
useMineAPI,
|
||||||
usePermissionAPI,
|
usePermissionAPI,
|
||||||
// usePreferenceAPI,
|
usePreferenceAPI,
|
||||||
// useSecurity,
|
// useSecurity,
|
||||||
useSnackbar,
|
useSnackbar,
|
||||||
// useTagAPI,
|
// useTagAPI,
|
||||||
|
|
|
||||||
|
|
@ -1564,7 +1564,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
<Grid size={{ xs: 7 }} container justifyContent="flex-end">
|
||||||
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
|
<CancelSubmit onSubmit={submit} onCancel={close} submitDisabled={!canEdit || !isInteractionValid(interaction) || invalidConditionValues()}/>
|
||||||
<Button onClick={close} color="primary">
|
{/* <Button onClick={close} color="primary">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
|
|
@ -1574,7 +1574,7 @@ export default function InteractionSettings(props: Props) {
|
||||||
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
|
disabled={!isInteractionValid(interaction) || invalidConditionValues()}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import FieldsIcon from "products/AgIcons/FieldsIcon";
|
||||||
import PlaneIcon from "products/AviationIcons/PlaneIcon";
|
import PlaneIcon from "products/AviationIcons/PlaneIcon";
|
||||||
import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
|
import AirportMapIcon from "products/AviationIcons/AirportMapIcon";
|
||||||
import JobsiteIcon from "products/Construction/JobSiteIcon";
|
import JobsiteIcon from "products/Construction/JobSiteIcon";
|
||||||
|
import { getThemeType } from "theme";
|
||||||
|
|
||||||
const drawerWidth = 230;
|
const drawerWidth = 230;
|
||||||
|
|
||||||
|
|
@ -207,7 +208,7 @@ export default function SideNavigator(props: Props) {
|
||||||
classes={getClasses("/team")}
|
classes={getClasses("/team")}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<People />
|
<People style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
{open && <ListItemText primary="Teams" />}
|
{open && <ListItemText primary="Teams" />}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
|
@ -219,7 +220,7 @@ export default function SideNavigator(props: Props) {
|
||||||
classes={getClasses("/user")}
|
classes={getClasses("/user")}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Person />
|
<Person style={{ color: getThemeType() === "light" ? "black" : undefined }}/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
{open && <ListItemText primary="Users" />}
|
{open && <ListItemText primary="Users" />}
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export default function DevicePage() {
|
||||||
const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false)
|
const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false)
|
||||||
|
|
||||||
const loadDevice = () => {
|
const loadDevice = () => {
|
||||||
console.log("load device page data")
|
// console.log("load device page data")
|
||||||
if (loading) return
|
if (loading) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => {
|
deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes()).then(resp => {
|
||||||
|
|
@ -120,7 +120,7 @@ export default function DevicePage() {
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
setDevice(Device.create());
|
setDevice(Device.create());
|
||||||
// setComponents(new Map());
|
// setComponents(new Map());
|
||||||
// setInteractions([]);
|
setInteractions([]);
|
||||||
setAvailablePositions(new Map());
|
setAvailablePositions(new Map());
|
||||||
setPermissions([]);
|
setPermissions([]);
|
||||||
setPreferences(pond.UserPreferences.create());
|
setPreferences(pond.UserPreferences.create());
|
||||||
|
|
@ -439,6 +439,8 @@ export default function DevicePage() {
|
||||||
refreshCallback={loadDevice}
|
refreshCallback={loadDevice}
|
||||||
availablePositions={availablePositions}
|
availablePositions={availablePositions}
|
||||||
availableOffsets={availableOffsets}
|
availableOffsets={availableOffsets}
|
||||||
|
components={[...components.values()]}
|
||||||
|
interactions={interactions}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export {
|
||||||
// useMeasurementsWebsocket,
|
// useMeasurementsWebsocket,
|
||||||
// useMetricAPI,
|
// useMetricAPI,
|
||||||
usePermissionAPI,
|
usePermissionAPI,
|
||||||
// usePreferenceAPI,
|
usePreferenceAPI,
|
||||||
useSiteAPI, //TODO: update api with resolve, reject
|
useSiteAPI, //TODO: update api with resolve, reject
|
||||||
useTagAPI,
|
useTagAPI,
|
||||||
useTeamAPI,
|
useTeamAPI,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useHTTP, usePermissionAPI } from "hooks";
|
import { useHTTP, usePermissionAPI } from "hooks";
|
||||||
// import { useWebsocket } from "websocket";
|
// import { useWebsocket } from "websocket";
|
||||||
import { /*Component,*/ deviceScope, User } from "models";
|
import { /*Component,*/ Component, deviceScope, User } from "models";
|
||||||
import { pond } from "protobuf-ts/pond";
|
import { pond } from "protobuf-ts/pond";
|
||||||
import { createContext, PropsWithChildren, useContext } from "react";
|
import { createContext, PropsWithChildren, useContext } from "react";
|
||||||
import { pondURL } from "./pond";
|
import { pondURL } from "./pond";
|
||||||
|
|
@ -8,6 +8,7 @@ import { AxiosResponse } from "axios";
|
||||||
import { useGlobalState } from "providers";
|
import { useGlobalState } from "providers";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { or } from "utils/types";
|
import { or } from "utils/types";
|
||||||
|
import { dateRange } from "providers/http";
|
||||||
// import { reject, result } from "lodash";
|
// import { reject, result } from "lodash";
|
||||||
|
|
||||||
export interface IDeviceAPIContext {
|
export interface IDeviceAPIContext {
|
||||||
|
|
@ -113,16 +114,16 @@ export interface IDeviceAPIContext {
|
||||||
isOverLimit: (id: number) => Promise<any>;
|
isOverLimit: (id: number) => Promise<any>;
|
||||||
isPaused: (id: number) => Promise<any>;
|
isPaused: (id: number) => Promise<any>;
|
||||||
setDatacap: (id: number, newCap: number) => Promise<any>;
|
setDatacap: (id: number, newCap: number) => Promise<any>;
|
||||||
// listJSONMeasurements: (
|
listJSONMeasurements: (
|
||||||
// id: number,
|
id: number,
|
||||||
// components: Component[],
|
components: Component[],
|
||||||
// startDate: any,
|
startDate: any,
|
||||||
// endDate: any,
|
endDate: any,
|
||||||
// limit: number,
|
limit: number,
|
||||||
// offset: number,
|
offset: number,
|
||||||
// order: string,
|
order: string,
|
||||||
// orderBy: string
|
orderBy: string
|
||||||
// ) => Promise<AxiosResponse<pond.ListDeviceExportedMeasurementsResponse>>;
|
) => Promise<AxiosResponse<pond.ListDeviceExportedMeasurementsResponse>>;
|
||||||
listSimpleJSON: (device: number) => Promise<any>;
|
listSimpleJSON: (device: number) => Promise<any>;
|
||||||
resetQuackCount: (id: number) => Promise<any>;
|
resetQuackCount: (id: number) => Promise<any>;
|
||||||
resetQuackCountTx: (id: number) => Promise<any>;
|
resetQuackCountTx: (id: number) => Promise<any>;
|
||||||
|
|
@ -755,65 +756,65 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// const listJSONMeasurements = (
|
const listJSONMeasurements = (
|
||||||
// id: number,
|
id: number,
|
||||||
// components: Component[],
|
components: Component[],
|
||||||
// startDate: any,
|
startDate: any,
|
||||||
// endDate: any,
|
endDate: any,
|
||||||
// limit: number,
|
limit: number,
|
||||||
// offset: number,
|
offset: number,
|
||||||
// order: string,
|
order: string,
|
||||||
// orderBy: string
|
orderBy: string
|
||||||
// ) => {
|
) => {
|
||||||
// let keyString = "";
|
let keyString = "";
|
||||||
// components.forEach((comp, i) => {
|
components.forEach((comp, i) => {
|
||||||
// if (i === 0) {
|
if (i === 0) {
|
||||||
// keyString = keyString + comp.key();
|
keyString = keyString + comp.key();
|
||||||
// } else {
|
} else {
|
||||||
// keyString = keyString + "," + comp.key();
|
keyString = keyString + "," + comp.key();
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
// if (as) {
|
if (as) {
|
||||||
// return get<pond.ListDeviceExportedMeasurementsResponse>(
|
return get<pond.ListDeviceExportedMeasurementsResponse>(
|
||||||
// pondURL(
|
pondURL(
|
||||||
// "/devices/" +
|
"/devices/" +
|
||||||
// id +
|
id +
|
||||||
// "/measurements/exportComplex" +
|
"/measurements/exportComplex" +
|
||||||
// dateRange(startDate, endDate) +
|
dateRange(startDate, endDate) +
|
||||||
// "&components=" +
|
"&components=" +
|
||||||
// keyString +
|
keyString +
|
||||||
// "&limit=" +
|
"&limit=" +
|
||||||
// limit +
|
limit +
|
||||||
// "&offset=" +
|
"&offset=" +
|
||||||
// offset +
|
offset +
|
||||||
// "&order=" +
|
"&order=" +
|
||||||
// order +
|
order +
|
||||||
// "&orderBy=" +
|
"&orderBy=" +
|
||||||
// orderBy +
|
orderBy +
|
||||||
// "&as=" +
|
"&as=" +
|
||||||
// as
|
as
|
||||||
// )
|
)
|
||||||
// );
|
);
|
||||||
// }
|
}
|
||||||
// return get<pond.ListDeviceExportedMeasurementsResponse>(
|
return get<pond.ListDeviceExportedMeasurementsResponse>(
|
||||||
// pondURL(
|
pondURL(
|
||||||
// "/devices/" +
|
"/devices/" +
|
||||||
// id +
|
id +
|
||||||
// "/measurements/exportComplex" +
|
"/measurements/exportComplex" +
|
||||||
// dateRange(startDate, endDate) +
|
dateRange(startDate, endDate) +
|
||||||
// "&components=" +
|
"&components=" +
|
||||||
// keyString +
|
keyString +
|
||||||
// "&limit=" +
|
"&limit=" +
|
||||||
// limit +
|
limit +
|
||||||
// "&offset=" +
|
"&offset=" +
|
||||||
// offset +
|
offset +
|
||||||
// "&order=" +
|
"&order=" +
|
||||||
// order +
|
order +
|
||||||
// "&orderBy=" +
|
"&orderBy=" +
|
||||||
// orderBy
|
orderBy
|
||||||
// )
|
)
|
||||||
// );
|
);
|
||||||
// };
|
};
|
||||||
|
|
||||||
const listSimpleJSON = (device: number) => {
|
const listSimpleJSON = (device: number) => {
|
||||||
return new Promise<AxiosResponse>((resolve, reject) => {
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
|
@ -925,7 +926,7 @@ export default function DeviceProvider(props: PropsWithChildren<Props>) {
|
||||||
isOverLimit,
|
isOverLimit,
|
||||||
isPaused,
|
isPaused,
|
||||||
setDatacap,
|
setDatacap,
|
||||||
// listJSONMeasurements,
|
listJSONMeasurements,
|
||||||
listSimpleJSON,
|
listSimpleJSON,
|
||||||
resetQuackCount,
|
resetQuackCount,
|
||||||
resetQuackCountTx,
|
resetQuackCountTx,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import TerminalProvider, {useTerminalAPI} from "./terminalAPI"
|
||||||
import SiteProvider, {useSiteAPI} from "./siteAPI";
|
import SiteProvider, {useSiteAPI} from "./siteAPI";
|
||||||
import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI";
|
import ObjectHeaterProvider, {useObjectHeaterAPI} from "./ObjectHeaterAPI";
|
||||||
import MutationProvider, {useMutationAPI} from "./mutationAPI"
|
import MutationProvider, {useMutationAPI} from "./mutationAPI"
|
||||||
|
import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI";
|
||||||
// import NoteProvider from "providers/noteAPI";
|
// import NoteProvider from "providers/noteAPI";
|
||||||
|
|
||||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||||
|
|
@ -72,7 +73,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
||||||
<SiteProvider>
|
<SiteProvider>
|
||||||
<ObjectHeaterProvider>
|
<ObjectHeaterProvider>
|
||||||
<MutationProvider>
|
<MutationProvider>
|
||||||
|
<PreferenceProvider>
|
||||||
{children}
|
{children}
|
||||||
|
</PreferenceProvider>
|
||||||
</MutationProvider>
|
</MutationProvider>
|
||||||
</ObjectHeaterProvider>
|
</ObjectHeaterProvider>
|
||||||
</SiteProvider>
|
</SiteProvider>
|
||||||
|
|
@ -131,5 +134,6 @@ export {
|
||||||
useTerminalAPI,
|
useTerminalAPI,
|
||||||
useSiteAPI,
|
useSiteAPI,
|
||||||
useObjectHeaterAPI,
|
useObjectHeaterAPI,
|
||||||
useMutationAPI
|
useMutationAPI,
|
||||||
|
usePreferenceAPI
|
||||||
};
|
};
|
||||||
|
|
|
||||||
49
src/providers/pond/preferenceAPI.tsx
Normal file
49
src/providers/pond/preferenceAPI.tsx
Normal 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);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue