device actions fully imported
This commit is contained in:
parent
59d9bf2e6b
commit
9cdf7514b2
7 changed files with 522 additions and 76 deletions
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,8 +34,8 @@ 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";
|
||||
|
|
@ -67,6 +67,9 @@ 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()
|
||||
|
|
@ -457,7 +460,7 @@ export default function DeviceActions(props: Props) {
|
|||
device={device}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
{/* <PauseData
|
||||
<PauseData
|
||||
id={device.id()}
|
||||
isOpen={isPauseDialogOpen}
|
||||
close={closeAndRefresh("isPauseDialogOpen")}
|
||||
|
|
@ -485,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
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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue