frontend/src/device/UpgradeDevice.tsx

345 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
IconButton,
LinearProgress,
Link,
Theme,
Tooltip,
Typography
} from "@mui/material";
// import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import UpdateIcon from "@mui/icons-material/Update";
import { useDeviceAPI, useFirmwareAPI, usePrevious, useSnackbar } from "hooks";
import { Device, Firmware, latestFirmwareVersion, Upgrade } from "models";
import moment from "moment";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { useGlobalState } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { or } from "utils/types";
import { makeStyles } from "@mui/styles";
const useStyles = makeStyles((theme: Theme) => {
return ({
dialogContent: {
marginTop: theme.spacing(2)
},
buttonProgress: {
color: theme.palette.primary.main,
position: "absolute",
zIndex: 1
},
failed: {
color: theme.palette.error.main
}
})
});
interface Props {
device: Device;
}
export default function UpgradeDevice(props: Props) {
const classes = useStyles();
const deviceAPI = useDeviceAPI();
const { success, error } = useSnackbar();
const firmwareAPI = useFirmwareAPI();
// const [{ firmware }] = useGlobalState();
const { device } = props;
const prevDevice = usePrevious(device);
const [open, setOpen] = useState<boolean>(false);
const prevOpen = usePrevious(open);
const [upgrade, setUpgrade] = useState<Upgrade>(Upgrade.create());
const [gettingStatus, setGettingStatus] = useState(0);
const [firmware, setFirmware] = useState<Map<string, Firmware>>(new Map())
const [firmwareLoading, setFirmwareLoading] = useState(false)
const prevFirmware = usePrevious(firmware);
const progress = useCallback(() => {
return upgrade.status.progress;
}, [upgrade.status.progress]);
const currentFirmware = () => {
return device.status.firmwareVersion;
};
const latestFirmware = () => {
return latestFirmwareVersion(
firmware,
device.settings.platform,
device.settings.upgradeChannel
);
};
useEffect(() => {
if (firmwareLoading) return
// if (firmware.size > 0) return
setFirmwareLoading(true)
firmwareAPI.getAllLatestFirmware().then(resp => {
let versions: Map<string, Firmware> = new Map();
if (resp && resp.data && resp.data.firmware) {
resp.data.firmware.forEach((raw: any) => {
let firmware = Firmware.any(raw);
let key = firmware.settings.platform + ":" + firmware.settings.channel;
versions.set(key, firmware);
});
}
// dispatch({ key: "firmware", value: versions });
setFirmware(versions)
}).finally(() => {
setFirmwareLoading(false)
})
}, [])
const upgradeVersion = () => {
return upgrade.settings.version;
};
const available = () => {
if (firmware.size < 1) return false
return currentFirmware() !== latestFirmware();
};
const upgrading = useCallback(() => {
return progress() === "upgrading";
}, [progress]);
const completed = () => {
return progress() === "completed" && currentFirmware() === latestFirmware();
};
const failed = () => {
return progress() === "failed" && upgradeVersion() === latestFirmware();
};
const percent = () => {
let progress = Math.round((upgrade.status.offset / upgrade.status.size) * 100)
return progress;
};
const tooltip = () => {
return upgrading()
? "Upgrade in progress: " + percent() + "%"
: failed()
? "Upgrade failed"
: "Upgrade Device";
};
const upgradeTimingDesc = () => {
let desc = "requested " + moment(or(upgrade.status.started, 0)).fromNow();
if (completed()) {
desc = "completed " + moment(or(upgrade.status.stopped, 0)).fromNow();
} else if (failed()) {
desc = "failed " + moment(or(upgrade.status.changed, 0)).fromNow();
}
return desc;
};
const mode = () => {
let mode = "prompt";
if (upgrading() || failed() || completed()) {
mode = "progress";
}
return mode;
};
const getStatus = useCallback(() => {
if (gettingStatus === device.id() && !upgrading()) {
return;
}
setGettingStatus(device.id());
deviceAPI
.getUpgradeStatus(device.id(), getContextKeys(), getContextTypes())
.then((res: any) => {
if (res && res.data && res.data.upgrade) {
setUpgrade(Upgrade.any(res.data.upgrade));
}
})
.catch((err: any) => error(err));
}, [device, deviceAPI, error, gettingStatus, upgrading]);
useEffect(() => {
const deviceIdUnchanged = !prevDevice || prevDevice.id() === device.id();
if (
(open && !prevOpen) ||
firmware !== prevFirmware ||
(device !== prevDevice && deviceIdUnchanged)
) {
getStatus();
}
}, [open, prevOpen, getStatus, device, prevDevice, firmware, prevFirmware]);
useEffect(() => {
function getStatusAgain() {
if (upgrading()) {
getStatus();
}
}
const interval = setInterval(() => getStatusAgain(), 5000);
return () => {
clearInterval(interval);
};
}, [open, getStatus, upgrading]);
const close = () => {
setOpen(false);
};
const handleUpgrade = () => {
firmwareAPI
.upgradeFirmware(device.id())
.then(() => {
success(device.name() + " will upgrade to " + latestFirmware() + " next time it checks in");
getStatus();
})
.catch((err: any) => {
console.error(err);
error(err);
});
};
const restart = () => {
firmwareAPI
.upgradeFirmware(device.id())
.then(() => {
success(
"Upgrade successfully restarted, " +
device.name() +
" will upgrade next time it checks in"
);
getStatus();
})
.catch((err: any) => {
console.error(err);
error(err);
});
};
const cancel = () => {
firmwareAPI
.cancelUpgrade(device.id())
.then(() => {
success("Upgrade cancelled");
})
.catch((err: any) => {
console.error(err);
error(err);
});
};
const button = () => {
return (
<Tooltip title={tooltip()}>
<IconButton
color="default"
className={failed() ? classes.failed : undefined}
onClick={() => setOpen(true)}
aria-label={tooltip()}>
<UpdateIcon />
{upgrading() && (
<CircularProgress
variant="determinate"
value={percent()}
size={"2rem"}
className={classes.buttonProgress}
/>
)}
</IconButton>
</Tooltip>
);
};
return (
<React.Fragment>
{(available() || upgrading() || failed()) && button()}
<Dialog fullWidth open={open} onClose={close} aria-labelledby="form-dialog-title">
{mode() === "prompt" && (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Upgrade Device
<Typography variant="body2" color="textSecondary">
{device.name()}
</Typography>
</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
Upgrading {device.platformName()} from {currentFirmware()} to {latestFirmware()}
<br />
<br />
It is recommended that the device be plugged in during the upgrade as it will take
several minutes to complete
<br />
<br />
While the upgrade is in progress you can return to this dialog to view its status
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={close} color="primary">
Close
</Button>
<Button onClick={handleUpgrade} color="primary">
Upgrade
</Button>
</DialogActions>
</React.Fragment>
)}
{mode() === "progress" && (
<React.Fragment>
<DialogTitle id="form-dialog-title">
Upgrading Device
<Typography variant="body2" color="textSecondary">
{device.name()}
</Typography>
</DialogTitle>
<Divider />
<DialogContent className={classes.dialogContent}>
<DialogContentText id="alert-dialog-slide-description">
Upgrade to {latestFirmware()} {upgradeTimingDesc()}
</DialogContentText>
<br />
<LinearProgress variant="determinate" value={percent()} />
<DialogContentText>
{or(upgrade.status.offset, 0)} / {upgrade.status.size} bytes ({percent()}%)
</DialogContentText>
{failed() && (
<React.Fragment>
<DialogContentText>
<br />
Uh oh something went wrong. You can try to{" "}
<Link variant="body2" onClick={restart} style={{ cursor: "pointer" }}>
restart
</Link>{" "}
the upgrade, if the problem persists we'll be alerted and follow up as soon as
possible.
</DialogContentText>
</React.Fragment>
)}
</DialogContent>
<DialogActions>
<Button onClick={close} color="primary">
Close
</Button>
{upgrading() && (
<Button onClick={cancel} style={{ color: "red" }}>
Cancel
</Button>
)}
{failed() && (
<Button onClick={restart} color="primary">
Restart
</Button>
)}
</DialogActions>
</React.Fragment>
)}
</Dialog>
</React.Fragment>
);
}