device actions fully imported

This commit is contained in:
Carter 2025-04-02 16:09:18 -06:00
parent 59d9bf2e6b
commit 9cdf7514b2
7 changed files with 522 additions and 76 deletions

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